Starknet implementation of the Opaque protocol, starting with the PSR
(Programmable Stealth Reputation) verification stack. Design rationale and
the full integration plan live in the spec repo:
spec/starknet-integration.md.
Status: P0 proof-of-concept. Nothing here is deployed.
| Package | Purpose | Status |
|---|---|---|
contracts/psr_groth16_verifier |
Groth16-BN254 verifier for the PSR V2 circuit, code-generated by Garaga from the pinned production verification key | Compiles |
contracts/reputation_verifier |
OpaqueReputationVerifierV2 port: root registry (1h TTL, delete-on-evict), u256 nullifier set, schema-liveness seam (OPQ-006) |
Fork-tested |
contracts/stealth_announcer |
CSAP announcer: EIP-5564-shaped Announcement event, Solana-style input bounds |
Tested |
contracts/stealth_registry |
CSAP meta-address registry: ERC-6538-shaped storage, 66/98-byte length check, SNIP-12 on-behalf registration through SRC-6 is_valid_signature with consumable nonces |
Tested |
contracts/stealth_account |
CSAP stealth custody: non-upgradeable OZ EthAccountComponent account, one-time P_stealth signer, counterfactual per-payment address |
Tested + declared |
contracts/psr_gate |
Tier-1 PSR consumer: credential-gated entry — enter only by proving a valid attestation under a required schema, one-time per nullifier (the integration thesis) | Live |
contracts/ons_mirror |
Read-only ONS mirror fed by native L1→L2 messaging (#[l1_handler]): emitter allowlist, monotonic sequence, revoke-tombstone (OPQ-004). Paired with StarknetOnsMirrorSender in opaquecash/ethereum |
Live |
psr_groth16_verifier is generated code. It is produced from the same
verification key pinned by Groth16VerifierV2.sol (Ethereum) and the Solana
groth16-verifier program, via the committed proof fixture in
circuits/test/fixtures/v2/. Do not edit it by hand; regenerate it:
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python --prerelease=allow 'garaga==1.1.0'
cd contracts
../.venv/bin/garaga gen --system groth16 \
--vk ../../circuits/test/fixtures/v2/verification_key.json \
--project-name psr_groth16_verifierA new trusted-setup ceremony (the planned mainnet ceremony for the OPQ-018 / OPQ-006 circuit fixes) changes the verification key and therefore requires regenerating and redeclaring this contract — it is not a parameter update.
After regenerating, re-add [lib] to the generated Scarb.toml (above the
[[target.starknet-contract]] section): an explicit contract target suppresses
the default lib target, and reputation_verifier consumes this package as a
dependency for its fork tests.
The verifier delegates multi-scalar multiplication to Garaga's universal ECIP
ops class (library_call to a pinned class hash), which is pre-declared on
Starknet mainnet and Sepolia by the Garaga project.
Pinned in contracts/psr_groth16_verifier/.tool-versions:
- scarb 2.16.1
- starknet-foundry 0.57.0
- garaga 1.1.0 (Python CLI, Python 3.12)
cd contracts/psr_groth16_verifier && scarb build
cd contracts/reputation_verifier && scarb build- Garaga verifier generated from the pinned V2 vkey
-
scarb buildgreen - Wrapper contract: root registry + nullifier set + schema-liveness seam, fork-tested with the real proof: nullifier consume + replay rejection + TTL expiry + unknown-root rejection + admin gating (5 tests green)
- Real V2 fixture proof encoded with
garaga calldata: 1,977 felts (contracts/psr_groth16_verifier/tests/proof_calldata.txt) - Real V2 fixture proof verified via snforge fork test against Sepolia
(real ECIP class): ~43.1M L2 gas (
snforge test) - Live declare/deploy + invoke on Starknet Sepolia: the committed V2
fixture proof verified on-chain and its replay was rejected with
'nullifier already used'(see deployments below)
Measured numbers: a single verification executes at ~43.1M L2 gas in the snforge fork (call execution only; the spec's ~34M order-of-magnitude came from Garaga's smaller benchmark vkey). The live end-to-end invoke — account validation, 1,978 felts of calldata, wrapper + verifier + ECIP library call, events — consumed 155.47M L2 gas / 4.81 STRK on Sepolia.
| Contract | Class hash | Address |
|---|---|---|
Groth16VerifierBN254 |
0x003c72da2c846e3304885e59fb3e0dae07243d482adc29ef3c248d60ad99992d |
0x01f339dfc3a1509bc3ccd1c7ea1a19c07bc0f89ad7378b505b3edc5f5b13b02e |
OpaqueReputationVerifierV2 |
0x04361bf98499842b39bd103f4ddc890a0adf56e060a75d98fca8143fc45fb576 |
0x079ada2245bd7f7575c4c67c9ea34edc9c420c57de711762b8ffbb8483823af6 |
PsrGate |
0x0222458c219f5c760436f616862d7a9f2f1df8ab311ebcfc7dd83ac1cef08a6b |
0x07103219e851b7bc16b07748e4427d38c2b884b06d524b52dbae9a04386b8331 |
OpaqueNameMirror |
0x01d0821469d516a81cddfd6ee6d88b5073657db561d9de2f6e723649ad4ff14a |
0x0355472f6f63510795a34d3a39a62424a1658f1311b9237bc87509811c508328 |
StealthAccount (class only) |
0x04794bab07198e0585d2d7951dbc5860fba47fea2a15d227ca3237b7b9e484ed |
counterfactual per payment |
StealthAnnouncer |
0x625f476e46225bf2c050d956201d6be75183dd50d9f20d31c408f09fcbaa4bb |
0x003b8258e84e6feec93239b442e6a91f532fda35fed67de4093b1d97150d2aa2 |
StealthMetaAddressRegistry |
0x6203ad71f44b9b4371a9b2f0394cef08045c3c8e14b86f34fee7517acafb819 |
0x047ff90c491384ecf8dba8b32b1eea7947f850ea92ddc196edcb0f508acff874 |
The OpaqueReputationVerifierV2 above is the current build (adds
verify_and_consume, which PsrGate uses); the earlier instance
0x017a56e5…83eb6 carried the first live proof verification below.
First live announcement: CSAP canonical vector 1 announced through the
@opaquecash/stealth-chain-starknet call builder and decoded back by its
adapter (tx 0x006efbb9…42d8,
block 12158304).
First live stealth payment: OpaqueClient.buildStarknetStealthSend produced
an ERC-20 transfer + announce multicall (tx 0x6c7b7905…98fa2,
block 12167420) that funded the counterfactual stealth account
0x352ca9…abfd5 with 0.001 STRK; the recipient then scanned Starknet and
owned the payment end-to-end.
First live sweep: the recipient reconstructed the stealth account from an
owned output (OpaqueClient.buildStarknetSweep), self-deployed it
(deploy_account 0x6713f89c…)
paid from its own balance, and transferred the funds out
(0x3a4cf7f1…) —
the full send → scan → own → sweep lifecycle proven on-chain.
First live PSR-gated entry (the integration thesis): PsrGate.enter accepted
the committed V2 credential proof
(tx 0x0668c1f0…) —
has_entered flipped true and entry_count reached 1 — while a replay was
rejected on-chain with nullifier already used. A stealth identity enters
only by proving, in zero knowledge, a valid attestation under the gate's
schema, once per scope.
ONS mirror (L1→L2): OpaqueNameMirror is paired with StarknetOnsMirrorSender
on Ethereum Sepolia (0xbdB9D13B8b5f94cBAf924d5AAEA216f66B9131Ef), allowlisted
as the mirror's L1 emitter. First live delivery: a name upsert published from
the sender (L1 tx 0x74b307bf…)
travelled the native L1→L2 path (Ethereum Starknet Core → sequencer →
handle_mirror); resolve_meta on the mirror then returned the exact spend/view
keys sent from Ethereum, so the two halves are proven end-to-end, not just
wire-compatible in tests.
The StealthAccount class hash is a consensus-critical CSAP constant
(spec/starknet-integration.md §7.1): every stealth address is a
counterfactual deployment of this exact class, so redeclaring a changed
class moves all future addresses.
Admin / deployer: sncast account opaque_deployer
(0x029db6e717afae61c5693afb65da25fb71974ccfe6705a8cc9282a8c9d725ceb,
local dev keystore — rotate to a multisig before anything real).
Live proof verification: tx 0x05586dae…79c56
(block 12156896) — ReputationVerified emitted, nullifier consumed
(is_nullifier_used → true), replay rejected at estimation.