Restrict circuit access using witness-derived keypairs.
Security warning — ownPublicKey() does not authorize the caller.ownPublicKey() returns a value the prover knows, not a proof of key
ownership. Any value stored on the public ledger can be replayed by anyone
reading the chain. Do not use ownPublicKey() for authorization checks
(assert(ownPublicKey() == storedAdmin)). Use it only as an identifier for
the operation’s target (e.g. “mint to this caller”). For authorization, see
Owner-Only Pattern below.
pragma language_version >= 0.22.0;import CompactStandardLibrary;struct AdminSecretKey { bytes: Bytes<32>; }struct AdminPublicKey { bytes: Bytes<32>; }// The ledger stores only the derived public key (a hash of the secret),// never the secret itself.export ledger contractAdmin: AdminPublicKey;// The deployer's DApp generates this secret and keeps it in private state.witness getAdminSecret(): AdminSecretKey;constructor() { contractAdmin = disclose(deriveAdminPublicKey(getAdminSecret()));}export circuit deriveAdminPublicKey(sk: AdminSecretKey): AdminPublicKey { return AdminPublicKey { bytes: persistentHash<Vector<2, Bytes<32>>>([ pad(32, "myapp:admin:pk:v1"), sk.bytes ]) };}export circuit privilegedAction(): [] { // To pass, the caller must supply a witness `sk` such that // H("myapp:admin:pk:v1" || sk) == contractAdmin. assert( contractAdmin == deriveAdminPublicKey(getAdminSecret()), "Not authorized." ); // ... privileged logic}// Rotate the admin key without ever transmitting a private key.// The new admin generates their secret locally and shares only the// derived public key with the current admin.export circuit rotateAdmin(newAdmin: AdminPublicKey): [] { assert( contractAdmin == deriveAdminPublicKey(getAdminSecret()), "Not authorized to rotate admin." ); contractAdmin = disclose(newAdmin);}
The contract stores H(domain || secret) on the ledger, never the secret
itself. The admin’s DApp holds the secret in private state and re-derives
the public key on every call. The on-chain value gives an attacker only the
hash; hash preimage resistance means they cannot forge a matching secret.
The domain string "myapp:admin:pk:v1" binds the public key to this
contract. The same secret will not derive the same public key under a
different domain, so an admin key cannot be replayed across contracts.
The assertion succeeds only if the caller knows a secret whose hash matches
the stored value. Reading contractAdmin off-chain gives an attacker the
hash, not the preimage.
The new admin generates their secret locally and shares only the derived
public key with the current admin. Private keys never leave the holder’s
DApp.
The ledger stores H(domain || secret) — the hash of the admin’s secret.
To pass the assertion, the caller must provide a witness sk such that
H(domain || sk) equals the stored value. The on-chain value is a hash,
not the secret itself, so reading the ledger gives an attacker nothing they
can replay. The domain string prevents the same secret from being valid
across different contracts.This is what Solidity gets implicitly from msg.sender and EVM signature
verification. In Compact, you construct the check yourself.
Do not use this pattern. It is shown only to document what fails.
// DO NOT USE — this is brokenexport ledger admin: ZswapCoinPublicKey;constructor() { admin = disclose(ownPublicKey());}export circuit privileged(): [] { assert(ownPublicKey() == admin, "Only admin.");}
ownPublicKey() returns whatever value the prover claims to know. An
attacker reads admin from the public ledger, supplies it as their own
ownPublicKey(), and produces a mathematically valid proof. The proof
correctly demonstrates “the prover knows a value equal to admin” — which
anyone watching the chain does. The check provides no authorization.
ownPublicKey() is the correct primitive when you need a stable identifier
for the caller as the target of an operation:
“Mint a token to this caller”: mint(ownPublicKey(), tokenId)
“Credit this caller’s balance”: balances.insert(ownPublicKey(), ...)
“Record this caller as the recipient”: recipient = disclose(ownPublicKey())
It identifies who the operation is for. It does not prove who is allowed
to perform it. The moment you write assert(ownPublicKey() == ...), you
have crossed into authorization, and the pattern breaks.
The || check would reveal which role matched, so callerKey is disclosed
explicitly. That is safe: a derived public key equals the on-chain value
only when the caller is already authorized, and the on-chain value is
public anyway.
The keypair pattern above covers single-admin and role-based authorization.
For other situations:
Signature verification against a stored verifying key — when you need
multi-sig, cold-key signing, or cross-contract authority. The contract
stores a verifying key; privileged circuits accept a signature over the
operation as a parameter.
Commitment / nullifier patterns — when you need anonymous user
authorization with unlinkability across actions (anonymous voting,
single-use credentials). The contract stores commitments and tracks
nullifiers.