Skip to content

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-pre0.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 surfaceBreaking change?Action required
IQKBRegistry view ABI (isVerified / nullifierOf / trustedListRoot)No — selectors byte-identical to V5/V5.1None. Existing SDK readers + IdentityEscrowNFT.isVerified() consumers work unchanged against the new registry.
register(...) / rotateWallet(...) write ABIYes — function selectors changed (proof + public-signal tuple shape changed)Re-bind to qkbRegistryV5_2Abi from @qkb/sdk (or @qkb/contracts-sdk).
Witness builder / circuitYes — circuit emits 22 public signals (was 19); msg.sender no longer in-circuitUse buildWitnessV5_2 from @qkb/sdk (web-eng A7.3).
Constructor signature for deployer-toolingNo(IGroth16Verifier*, address admin, bytes32 trustRoot, bytes32 policyRoot) is parity-equivalent to V5.1None. 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.1None. Verified via forge inspect QKBRegistryV5_2 storageLayout against V5.1.
Contract address on Sepolia / mainnetYes — 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.

FunctionV5.1 selectorV5.2 selectorNotes
register(...)0x8843e7570x9ab660c7Public-signal tuple grew 19 → 22 fields ⇒ selector changed.
rotateWallet(...)0x07d19c500x9849ff37Same 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)0xb9209e330xb9209e33Unchanged.
nullifierOf(address)0x61e0a22e0x61e0a22eUnchanged.
trustedListRoot()0x4b7f352e0x4b7f352eUnchanged.
policyRoot()0xe98cdb8c0xe98cdb8cUnchanged.
identityCommitments(bytes32)0x4d0d48b80x4d0d48b8Unchanged.
identityWallets(bytes32)0x8afb1b910x8afb1b91Unchanged.
usedCtx(bytes32, bytes32)0x3af1740b0x3af1740bUnchanged.
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

SlotV5.1V5.2Notes
0msgSendertimestampV5.1 slot 0 dropped. Wallet bind moves from circuit-emitted slot to on-chain keccak-derive of slots 18..21.
1timestampnullifierV5.1 slots 1..18 shift down by 1.
2..16(V5.1 slot N)(V5.1 slot N − 1)Same shift.
17rotationOldCommitmentrotationNewWallet(V5.1 slot 18 → V5.2 slot 17)
18rotationNewWalletbindingPkXHiNEW: upper 128 bits of parser.pkBytes[1..33], big-endian.
19bindingPkXLoNEW: lower 128 bits of parser.pkBytes[1..33].
20bindingPkYHiNEW: upper 128 bits of parser.pkBytes[33..65].
21bindingPkYLoNEW: 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

ErrorV5.1V5.2Notes
BadSender() 0xf9b5d12d✅ presentremovedReplaced by WalletDerivationMismatch (semantics generalize).
WalletDerivationMismatch() 0x02373a16NEWReverts when the keccak-derived address from bindingPkX/Y limbs ≠ msg.sender. Subsumes BadSender.
WrongRegisterModeNoOp() 0xecd3e1ccNEWReverts 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() 0x071478a6NEWDefense-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:

SlotNameTypeV5.1V5.2
0adminaddress
1trustedListRootbytes32
2policyRootbytes32
3nullifierOfmapping(address => bytes32)
4identityCommitmentsmapping(bytes32 => bytes32)
5identityWalletsmapping(bytes32 => address)
6usedCtxmapping(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:

solidity
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) and PoseidonT7 (~23.6 KB); cache as immutables for cheap reads.
  • Set admin + initial trustedListRoot + initial policyRoot.
  • 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):

PathV5.1 (real tuple)V5.2 (real tuple)Δ
test_real_tuple_full_register_gas2,169,4612,198,170+28,709 (+1.32%)
test_real_tuple_groth16_verify_only_gas378,344405,248+26,904 (+7.11%)
test_register_gas_bisection_by_gate (sum)1,594,3441,603,399+9,055 (+0.57%)

V5.2 NEW per-gate cost (from bisection):

GateCost
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 ForceEqualIfEnabled constraint)

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.1V5.2
Constraint count4,022,1713,876,304 (−145,867)
Powers-of-taupot23 (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

  • IQKBRegistry interface — 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-SybilusedCtx[fp][ctxKey] bookkeeping unchanged.
  • First-claim nullifier semantics — V5.1 invariant 4 (nullifierOf is write-once on first-claim, migrates atomically on rotateWallet) preserved verbatim.
  • Real-Diia E2E suiteRealDiiaE2ETest (V4 baseline) unaffected; V5.2 is independent of V4.

Migration guidance

For SDK consumers (web, integrators)

  1. Bump dependency: @qkb/sdk@0.5.2-pre.
  2. If you only read (isVerified, nullifierOf, trustedListRoot): no changes. Existing qkbRegistryV5_1Abi calls work against the V5.2 deployment, but prefer qkbRegistryV5_2Abi for forward compatibility.
  3. If you write (register, rotateWallet): switch to qkbRegistryV5_2Abi. The 22-field public-signal struct is generated by the new witness builder (web-eng A7.3); follow the V5.2 web flow.
  4. If you classify reverts: add the 3 new V5.2 selectors to your error decoder:
    • WalletDerivationMismatch() 0x02373a16
    • WrongRegisterModeNoOp() 0xecd3e1cc
    • BindingPkLimbOutOfRange() 0x071478a6 Drop V5.1's BadSender() 0xf9b5d12d (no longer emitted).

For deployer-tooling

  1. Use script/DeployV5_2.s.sol instead of script/DeployV5.s.sol.
  2. Same env vars; same broadcast invocation pattern; same GROTH16_VERIFIER_ADDR-or-placeholder fallback.
  3. Production deploys MUST pass GROTH16_VERIFIER_ADDR pointing 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 at packages/contracts/snapshots/gas-snapshot.txt (commit 0118a93).
  • pnpm -F @qkb/sdk test: 179/179 pass (T3 ABI surface check, commit baed9d7).
  • Cross-package integrity: Groth16VerifierV5_2Stub.sol sha256 matches ceremony/v5_2/zkey.sha256 per 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

Released under the MIT License. zk-QES — a zero-knowledge protocol over qualified electronic signatures.