V5.2 contracts release — keccak-on-chain amendment
Renamed in v0.6.0 — content reflects v0.5.x state; subsequent versions use zkqes nomenclature.
Package: @qkb/contracts, @qkb/contracts-sdk, @qkb/sdkVersion: 0.5.1-pre → 0.5.2-preBranch: feat/v5_2arch-contracts (commits 024b62b..0118a93) Spec: docs/superpowers/specs/2026-05-01-keccak-on-chain-amendment.md (circuits-eng v0.4 — folds contracts-eng's two substantive review findings, locked at 8f5277f on feat/v5_2arch-circuits; merges to main with the V5.2 release) Contract review: docs/superpowers/specs/2026-05-01-keccak-on-chain-contract-review.md (PASS-WITH-NOTES, locked at dc16330 + 94032a1 retraction footer)
V5.2 is a focused cross-chain portability + circuit-size amendment. The wallet-pubkey → msg.sender keccak gate moves from V5.1's in-circuit Secp256k1AddressDerive (~145K constraints) to a one-line on-chain reconstruction — no security loss, ~9K gas added at the contract layer, pot22 ceremony power sufficient.
TL;DR for downstream consumers
| Consumer surface | Breaking change? | Action required |
|---|---|---|
IQKBRegistry view ABI (isVerified / nullifierOf / trustedListRoot) | No — selectors byte-identical to V5/V5.1 | None. Existing SDK readers + IdentityEscrowNFT.isVerified() consumers work unchanged against the new registry. |
register(...) / rotateWallet(...) write ABI | Yes — function selectors changed (proof + public-signal tuple shape changed) | Re-bind to qkbRegistryV5_2Abi from @qkb/sdk (or @qkb/contracts-sdk). |
| Witness builder / circuit | Yes — circuit emits 22 public signals (was 19); msg.sender no longer in-circuit | Use buildWitnessV5_2 from @qkb/sdk (web-eng A7.3). |
| Constructor signature for deployer-tooling | No — (IGroth16Verifier*, address admin, bytes32 trustRoot, bytes32 policyRoot) is parity-equivalent to V5.1 | None. DeployV5_2.s.sol mirrors DeployV5.s.sol verbatim minus name swaps. |
Storage layout (for off-chain indexers reading via eth_getStorageAt) | No — slots 0..6 byte-identical to V5.1 | None. Verified via forge inspect QKBRegistryV5_2 storageLayout against V5.1. |
| Contract address on Sepolia / mainnet | Yes — fresh deploy (registry is non-upgradeable) | Update fixtures/contracts/sepolia.json after the V5.2 Sepolia deploy lands. Holders re-register from V5.1 via the existing fresh-QES flow; no upgrade path. |
Why V5.2
Cross-chain portability. V5.1's Secp256k1AddressDerive baked an EVM-specific keccak gate (over the 64-byte uncompressed wallet pubkey) directly into the BN254 Groth16 circuit. The same circuit could not target a non-EVM-keccak chain — even if that chain has a BN254 pairing precompile and a P-256 ECDSA precompile (the other two V5 prerequisites). Moving keccak to the contract layer makes the circuit + zkey portable across BN254-Groth16 chains; the EVM-specific bind becomes one host-native op the new chain implements anyway.
(Caveat: the OTHER V5 host-portability gates — P-256 ECDSA precompile via RIP-7212 / EIP-7951, EVM-style address = keccak(pk)[12:32] caller-auth model — still apply. Non-EVM caller-auth models (Solana ed25519, Cosmos bech32, Move) need a per-chain auth-shim outside V5.2 scope. See contract review §"Cost estimate" for the full host-portability matrix.)
Smaller circuit, simpler ceremony. V5.2 sheds −145,867 constraints (3,876,304 vs V5.1's 4,022,171). The savings push the constraint count comfortably under pot22's 4,194,304 limit (7.6% headroom), so the V5.2 multi-contributor ceremony can use a pot22 (4.19M) powers-of-tau instead of V5.1's pot23 (8.39M). Smaller pot ⇒ smaller witness ⇒ smaller proving key ⇒ less bandwidth + storage in the contributor flow.
ABI changes
Function selectors
Verified via forge inspect QKBRegistryV5{,_2} methodIdentifiers.
| Function | V5.1 selector | V5.2 selector | Notes |
|---|---|---|---|
register(...) | 0x8843e757 | 0x9ab660c7 | Public-signal tuple grew 19 → 22 fields ⇒ selector changed. |
rotateWallet(...) | 0x07d19c50 | 0x9849ff37 | Same tuple-shape change ⇒ selector changed. (Rotation flow itself unchanged — bindingPk limbs are present in the proof but not consulted at gate time; the rotation auth ECDSA sig is the load-bearing primitive.) |
isVerified(address) | 0xb9209e33 | 0xb9209e33 | Unchanged. |
nullifierOf(address) | 0x61e0a22e | 0x61e0a22e | Unchanged. |
trustedListRoot() | 0x4b7f352e | 0x4b7f352e | Unchanged. |
policyRoot() | 0xe98cdb8c | 0xe98cdb8c | Unchanged. |
identityCommitments(bytes32) | 0x4d0d48b8 | 0x4d0d48b8 | Unchanged. |
identityWallets(bytes32) | 0x8afb1b91 | 0x8afb1b91 | Unchanged. |
usedCtx(bytes32, bytes32) | 0x3af1740b | 0x3af1740b | Unchanged. |
setTrustedListRoot, setPolicyRoot, transferAdmin | (admin) | (admin, identical selectors) | Unchanged. |
poseidonT3(), poseidonT7(), groth16Verifier(), admin() | (getters) | (getters, identical selectors) | Unchanged. |
Implication for SDK consumers: view-only readers + IdentityEscrowNFT.isVerified() consumers work against V5.2 with zero ABI binding changes. Only callers that write (i.e., that build register / rotateWallet calldata) need the new ABI.
Public-signal layout
| Slot | V5.1 | V5.2 | Notes |
|---|---|---|---|
| 0 | msgSender | timestamp | V5.1 slot 0 dropped. Wallet bind moves from circuit-emitted slot to on-chain keccak-derive of slots 18..21. |
| 1 | timestamp | nullifier | V5.1 slots 1..18 shift down by 1. |
| 2..16 | (V5.1 slot N) | (V5.1 slot N − 1) | Same shift. |
| 17 | rotationOldCommitment | rotationNewWallet | (V5.1 slot 18 → V5.2 slot 17) |
| 18 | rotationNewWallet | bindingPkXHi | NEW: upper 128 bits of parser.pkBytes[1..33], big-endian. |
| 19 | — | bindingPkXLo | NEW: lower 128 bits of parser.pkBytes[1..33]. |
| 20 | — | bindingPkYHi | NEW: upper 128 bits of parser.pkBytes[33..65]. |
| 21 | — | bindingPkYLo | NEW: lower 128 bits of parser.pkBytes[33..65]. |
The 4 new limbs are each Bits2Num(128)-packed in-circuit (big-endian convention, matches the contract's bytes16(uint128(...)) reconstruction). The contract reconstructs the 64-byte uncompressed pk (no SEC1 0x04 prefix) and runs keccak256(...)[12:32] to derive the address — full mirror of EVM's address = keccak(pubkey)[12:] derivation.
Custom errors
| Error | V5.1 | V5.2 | Notes |
|---|---|---|---|
BadSender() 0xf9b5d12d | ✅ present | ❌ removed | Replaced by WalletDerivationMismatch (semantics generalize). |
WalletDerivationMismatch() 0x02373a16 | — | ✅ NEW | Reverts when the keccak-derived address from bindingPkX/Y limbs ≠ msg.sender. Subsumes BadSender. |
WrongRegisterModeNoOp() 0xecd3e1cc | — | ✅ NEW | Reverts when register-mode rotationNewWallet (slot 17) ≠ uint160(msg.sender). Replaces V5.1's in-circuit ForceEqualIfEnabled ((1 - rotationMode) * (rotationNewWallet - msgSender) === 0), which can no longer reference the now-absent in-circuit msgSender. The paired rotationOldCommitment === identityCommitment no-op stays in-circuit (no msgSender dependency). |
BindingPkLimbOutOfRange() 0x071478a6 | — | ✅ NEW | Defense-in-depth revert when any of bindingPkX/Y Hi/Lo exceeds 2^128 - 1. The circuit's Bits2Num(128) constraint should ensure this; the explicit revert surfaces a hypothetical circuit bug as a diagnosable error rather than silent uint128 truncation. |
All other custom error selectors (BadProof, BadSignedAttrsHi/Lo, BadLeafSpki, BadIntSpki, BadLeafSig, BadIntSig, BadTrustList, BadPolicy, StaleBinding, FutureBinding, AlreadyRegistered, WrongMode, CommitmentMismatch, WalletNotBound, CtxAlreadyUsed, UnknownIdentity, InvalidNewWallet, InvalidRotationAuth, OnlyAdmin, ZeroAddress, PoseidonDeployFailed, PoseidonStaticcallFailed, PrecompileCallFailed, SpkiLength, SpkiPrefix) are byte-identical V5.1 → V5.2.
Storage layout
Verified via forge inspect QKBRegistryV5_2 storageLayout and diffed against V5.1 — identical, all 7 slots match by name, type, slot index, offset, and size:
| Slot | Name | Type | V5.1 | V5.2 |
|---|---|---|---|---|
| 0 | admin | address | ✓ | ✓ |
| 1 | trustedListRoot | bytes32 | ✓ | ✓ |
| 2 | policyRoot | bytes32 | ✓ | ✓ |
| 3 | nullifierOf | mapping(address => bytes32) | ✓ | ✓ |
| 4 | identityCommitments | mapping(bytes32 => bytes32) | ✓ | ✓ |
| 5 | identityWallets | mapping(bytes32 => address) | ✓ | ✓ |
| 6 | usedCtx | mapping(bytes32 => mapping(bytes32 => bool)) | ✓ | ✓ |
Note: the registry is non-upgradeable. Storage-layout parity is documented for off-chain indexers / eth_getStorageAt consumers — there is no in-place upgrade path. V5.1 → V5.2 transition is a fresh deploy + holder re-registration via the existing fresh-QES flow.
Constructor signature
Byte-identical V5.1 → V5.2:
constructor(
IGroth16VerifierV5_2 _verifier, // ← was IGroth16VerifierV5_1; same shape
address _admin,
bytes32 _initialTrustedListRoot,
bytes32 _initialPolicyRoot
)Side effects also unchanged:
- CREATE-deploy
PoseidonT3(~9.8 KB) andPoseidonT7(~23.6 KB); cache as immutables for cheap reads. - Set
admin+ initialtrustedListRoot+ initialpolicyRoot. - Revert
ZeroAddress()on zero verifier or zero admin.
DeployV5_2.s.sol (packages/contracts/script/DeployV5_2.s.sol, commit 9df1bfc) mirrors DeployV5.s.sol (V5/V5.1) verbatim except for the imports (verifier + registry types). Same env layout (PRIVATE_KEY, ADMIN_ADDRESS, INITIAL_TRUST_ROOT, INITIAL_POLICY_ROOT, MINT_DEADLINE, optional GROTH16_VERIFIER_ADDR, optional CHAIN_LABEL), same broadcast flow, same dev-fallback Groth16VerifierV5_2Placeholder when GROTH16_VERIFIER_ADDR is unset.
Gas measurements
Real-tuple snapshot via the V5.2 single-contributor stub-ceremony Groth16 verifier (Groth16VerifierV5_2Stub.sol), measured by RealTupleGasSnapshotV5_2.t.sol. Comparison against V5.1's RealTupleGasSnapshotTest (commit 04b4a71):
| Path | V5.1 (real tuple) | V5.2 (real tuple) | Δ |
|---|---|---|---|
test_real_tuple_full_register_gas | 2,169,461 | 2,198,170 | +28,709 (+1.32%) |
test_real_tuple_groth16_verify_only_gas | 378,344 | 405,248 | +26,904 (+7.11%) |
test_register_gas_bisection_by_gate (sum) | 1,594,344 | 1,603,399 | +9,055 (+0.57%) |
V5.2 NEW per-gate cost (from bisection):
| Gate | Cost |
|---|---|
| Gate 2a-prime (keccak-derive holder addr from bindingPkX/Y limbs) | 9,047 gas |
Decomposition of the +28,709 register delta:
- +9,047 gas — Gate 2a-prime keccak-derive (the core V5.2 amendment cost)
- +26,904 gas — Groth16 verify (22-input vs 19-input pairing equation: 3 extra IC scalar muls + 3 extra G1 adds)
- −7,242 gas — net offset (smaller calldata from msgSender drop; register-mode no-op gate is now a single SLOAD-free inequality instead of an in-circuit
ForceEqualIfEnabledconstraint)
Spec ceiling 2.5M (V5 §3, revised at commit def6270) holds — V5.2 register at 2.20M leaves ~14% regression headroom (vs V5.1's ~16%).
rotateWallet() not measured here (RealTupleGasSnapshotV5_2 covers register only). V5.2 rotateWallet is functionally equivalent to V5.1 — the rotation flow doesn't traverse the keccak-derive gate. Projected real-pairing rotateWallet ≈ V5.1's ~380K-400K range.
Constraint count + ceremony pot size
| V5.1 | V5.2 | |
|---|---|---|
| Constraint count | 4,022,171 | 3,876,304 (−145,867) |
| Powers-of-tau | pot23 (8,388,608) | pot22 (4,194,304) |
| Headroom | ~52% | ~7.6% |
V5.2's −145,867 constraint reduction comes entirely from removing V5.1 §6.8's Secp256k1AddressDerive keccak template (the in-circuit keccak gate that proved address = keccak(pkBytes)[12:32]). The contract picks up this responsibility at Gate 2a-prime, paying ~9K gas per call instead of ~145K constraints per proof.
What stayed the same
IQKBRegistryinterface — V5.2 implements it; selectors and return types byte-identical to V5/V5.1. Third-party contracts that depend only on this read interface (e.g.,IdentityEscrowNFT.isVerified(), external SDK readers) work unchanged.- Storage layout — all 7 slots byte-identical V5.1 → V5.2.
- Constructor signature —
(verifier, admin, trustRoot, policyRoot)parity-equivalent. - Poseidon deployment pattern — CREATE-deploy T3 + T7 in constructor; cached as immutables.
- rotateWallet auth pattern — ECDSA sig from oldWallet's privkey over the rotation payload; bindingPk limbs in the proof are not consulted at rotation gate time.
- Per-(identity, ctx) anti-Sybil —
usedCtx[fp][ctxKey]bookkeeping unchanged. - First-claim nullifier semantics — V5.1 invariant 4 (
nullifierOfis write-once on first-claim, migrates atomically onrotateWallet) preserved verbatim. - Real-Diia E2E suite —
RealDiiaE2ETest(V4 baseline) unaffected; V5.2 is independent of V4.
Migration guidance
For SDK consumers (web, integrators)
- Bump dependency:
@qkb/sdk@0.5.2-pre. - If you only read (
isVerified,nullifierOf,trustedListRoot): no changes. ExistingqkbRegistryV5_1Abicalls work against the V5.2 deployment, but preferqkbRegistryV5_2Abifor forward compatibility. - If you write (
register,rotateWallet): switch toqkbRegistryV5_2Abi. The 22-field public-signal struct is generated by the new witness builder (web-eng A7.3); follow the V5.2 web flow. - If you classify reverts: add the 3 new V5.2 selectors to your error decoder:
WalletDerivationMismatch()0x02373a16WrongRegisterModeNoOp()0xecd3e1ccBindingPkLimbOutOfRange()0x071478a6Drop V5.1'sBadSender()0xf9b5d12d(no longer emitted).
For deployer-tooling
- Use
script/DeployV5_2.s.solinstead ofscript/DeployV5.s.sol. - Same env vars; same broadcast invocation pattern; same
GROTH16_VERIFIER_ADDR-or-placeholder fallback. - Production deploys MUST pass
GROTH16_VERIFIER_ADDRpointing at the real ceremonied V5.2 verifier (post-Phase-B multi-contributor ceremony). The script logs a loud warning when the placeholder is wired in.
For holders
V5.1 → V5.2 is a fresh deploy, not an upgrade. Existing V5.1 holders re-register against the new V5.2 registry via the standard fresh-QES flow. The two registries can coexist on-chain during the transition window; downstream consumers point at one or the other via deployment-fixture configuration (fixtures/contracts/{sepolia,mainnet}.json).
Acceptance
forge build: clean (via_ir,optimizer_runs=200).forge test: 410 passed / 0 failed / 1 skipped / 411 total (1 skipped =test_deploy_base_sepolia_fork, env-gated). +30 V5.2 unit tests (QKBRegistryV5_2.t.sol) + 4 V5.2 real-tuple integration tests (RealTupleGasSnapshotV5_2.t.sol) over V5.1 baseline.forge snapshot: refreshed atpackages/contracts/snapshots/gas-snapshot.txt(commit0118a93).pnpm -F @qkb/sdk test: 179/179 pass (T3 ABI surface check, commitbaed9d7).- Cross-package integrity:
Groth16VerifierV5_2Stub.solsha256 matchesceremony/v5_2/zkey.sha256per circuits-eng's pump (lead-verified at pump time).
Outstanding before mainnet
- V5.2 multi-contributor Phase-B ceremony (circuits-eng §11 equivalent for V5.2) — produces the real production verifier, replacing the single-contributor stub.
- Sepolia E2E gate (orchestration §9.4) — full register flow against real V5.2 ceremonied verifier on Base Sepolia.
- web-eng A7.3 completion — V5.2 witness builder + register calldata encoder.
References
- V5 architecture spec:
docs/superpowers/specs/2026-04-29-v5-architecture-design.md - V5.1 wallet-bound amendment:
docs/superpowers/specs/2026-04-30-wallet-bound-nullifier-amendment.md - V5.2 keccak-on-chain amendment design (v0.4, locked at
8f5277f):docs/superpowers/specs/2026-05-01-keccak-on-chain-amendment.md - V5.2 contract-side review (PASS-WITH-NOTES):
docs/superpowers/specs/2026-05-01-keccak-on-chain-contract-review.md - V5 architecture orchestration:
docs/superpowers/plans/2026-04-29-v5-architecture-orchestration.md - Gas-acceptance revision (600K → 2.5M): commit
def6270