Problem
The current pda = [literal(...), account(...)] macro syntax only accepts public-derivable seed components — byte literals and account IDs. Both are known to the caller before the transaction is built, so PDA addresses can be computed outside the zkVM.
This means any PDA used as a double-spend nullifier (e.g. a vote receipt) leaks a deterministic one-way hash of the voter's private AccountId:
receipt_pda = SHA-256(pad("vote_receipt_v1") || voter_account_id || admin_account_id)
Because admin_account_id is public and voter_account_id is fixed per voter, anyone who already knows a voter's AccountId can check on-chain whether they voted. This is acceptable for passive observers (preimage-resistant), but it fails coercion-resistance: a targeted adversary with knowledge of the voter's AccountId can confirm participation.
Proposed solution
Add a new seed component type — nullifier_of("account") — that computes a session-scoped nullifier from the private account's NSK inside the zkVM:
#[account(init, pda = [literal("vote_receipt_v1"), nullifier_of("voter"), account("admin")])]
vote_receipt: AccountWithMetadata,
Where nullifier_of("voter") expands to something like:
H(NSK_voter || program_id || "vote_receipt_v1" || admin_account_id)
computed from the private account's Nullifier Secret Key inside the zkVM circuit. The resulting value is unlinkable to the voter's AccountId even by an adversary who knows that ID.
Required framework changes
- New seed type in the macro —
nullifier_of("account_param") alongside the existing literal(...) and account(...) variants.
- NSK access inside program execution — the program context needs to expose the NSK for private accounts (already available to the circuit for nullifier computation; needs surfacing to
#[instruction] bodies or the PDA derivation step).
- Output the computed PDA address — because the seed is computed inside the zkVM, the caller (FFI / client) cannot pre-compute the PDA. The framework must include the initialised account's address in the program output so the caller can reference it in subsequent transactions.
Motivation / use cases
- Private voting — vote receipt PDA is unlinkable to the voter's identity even under targeted surveillance.
- Private reputation / badges — claim a credential PDA without revealing which account claimed it.
- Anonymous rate-limiting — prove you haven't performed an action without revealing who you are.
Current workaround
Use the voter's public AccountId as a seed component (as done in joke-wall). This hides votes from passive observers but leaks participation to anyone who already knows the voter's AccountId.
Problem
The current
pda = [literal(...), account(...)]macro syntax only accepts public-derivable seed components — byte literals and account IDs. Both are known to the caller before the transaction is built, so PDA addresses can be computed outside the zkVM.This means any PDA used as a double-spend nullifier (e.g. a vote receipt) leaks a deterministic one-way hash of the voter's private
AccountId:Because
admin_account_idis public andvoter_account_idis fixed per voter, anyone who already knows a voter'sAccountIdcan check on-chain whether they voted. This is acceptable for passive observers (preimage-resistant), but it fails coercion-resistance: a targeted adversary with knowledge of the voter'sAccountIdcan confirm participation.Proposed solution
Add a new seed component type —
nullifier_of("account")— that computes a session-scoped nullifier from the private account's NSK inside the zkVM:Where
nullifier_of("voter")expands to something like:computed from the private account's Nullifier Secret Key inside the zkVM circuit. The resulting value is unlinkable to the voter's
AccountIdeven by an adversary who knows that ID.Required framework changes
nullifier_of("account_param")alongside the existingliteral(...)andaccount(...)variants.#[instruction]bodies or the PDA derivation step).Motivation / use cases
Current workaround
Use the voter's public
AccountIdas a seed component (as done in joke-wall). This hides votes from passive observers but leaks participation to anyone who already knows the voter'sAccountId.