Skip to content

feat(circom): CRL non-membership via compressed SMT (benchmark) - #876

Draft
kilianglas wants to merge 1 commit into
mainfrom
bench/crl-smt-compressed
Draft

feat(circom): CRL non-membership via compressed SMT (benchmark)#876
kilianglas wants to merge 1 commit into
mainfrom
bench/crl-smt-compressed

Conversation

@kilianglas

Copy link
Copy Markdown
Contributor

Candidate 3 of 3 for credential revocation list (CRL) support. Not for merge — one of three branches opened so the designs can be compared on measured constraint cost. Related: bench/crl-imt-low-leaf, bench/crl-smt-full.

What this proves

The credential is not revoked, where the revocation list is a compressed sparse Merkle tree following the Iden3 / Polygon ID revocation model (checkClaimNotRevoked). Leaves sit at the shortest depth that distinguishes them, so the level count tracks the number of revoked entries rather than the key width.

Non-membership has two cases, both handled:

  • is_old_0 = 1 — the path terminates on an empty node.
  • is_old_0 = 0 — the path terminates on a different leaf, and the circuit enforces key != old_key. This case exists only in a compressed tree; a full fixed-depth tree always terminates on an empty node.

What this vendors

The repo carries a partial copy of circomlib that omits Iden3 smt/, so this branch adds it under circom/smt/, plus the missing Switcher template:

  • smt_verifier.circom, smt_lev_ins.circom, smt_verifier_sm.circom, smt_verifier_level.circom — from iden3/circomlib, logic unchanged.
  • smt_hash_poseidon2.circom — the one real change: SMTHash1/SMTHash2 ported from classic Poseidon to the Poseidon2 primitives this repo uses. SMTHash2 uses the same compression mode as binary_merkle_root.circom, so both tree implementations agree on node hashing.

This is the largest amount of net-new / ported circuit code of the three candidates, and therefore the highest implementation risk.

Measured cost

circom 2.2.3, R1CS totals (non-linear + linear):

circuit constraints delta vs baseline
OPRFNullifierProof on main 73,767
gadget alone, nLevels 32 18,240
OPRFNullifierProof with gadget 92,007 +18,240 (+24.7%)

Level sweep (gadget alone): n24 14,232 · n28 16,236 · n32 18,240 · n40 22,248 · n48 26,256 — about 501 constraints per level.

That per-level rate is the notable result: the indexed Merkle tree costs ~493 and the full SMT ~493, so the per-level state machine here adds only ~1.6%. Cost is essentially just levels × 493 for all three candidates, which reduces the whole comparison to tree depth.

Sizing nLevels

Depth is probabilistic, and a key whose distinguishing path exceeds nLevels cannot be represented — a liveness failure. Measured over 7 trials with uniform 64-bit keys:

revoked entries IMT depth compressed SMT depth (min/median/max) max ratio
1,000 10 19 / 21 / 23 2.30
10,000 14 26 / 27 / 31 2.21
100,000 17 33 / 34 / 39 2.29
1,000,000 20 39 / 40 / 42 2.10

So nLevels needs roughly 2 × log2(N) plus margin — around 48 for a 1M-entry CRL, which puts this candidate at 26,256 constraints, not far below the full SMT variant. The 2x depth ratio previously assumed in the design notes is confirmed.

Hashing the key is a safety requirement, not just a collision fix: with a raw issuer-chosen id, an issuer could mint ids sharing a long prefix and deliberately push paths past nLevels. Truncating a hash to shrink depth is unsafe in the other direction — a birthday collision would cause false revocation.

Scope

Data structure and in-circuit gadget only. Tree maintenance — maintainer service, on-chain root anchoring, insert batching, reorg handling — is deliberately out of scope.

Updates are independent per key, and un-revocation is a local leaf removal with re-compression, which is materially simpler than the indexed tree.

Tests

circom/tests/tests/crl_non_membership_smt_compressed.test.js covers both non-membership cases plus rejection of a revoked key, a forged is_old_0, a wrong root and a tampered sibling. The existing oprf_nullifier KAT is extended with the CRL witness and still produces the same nullifier. Full suite green.

Fixtures come from an independent compressed-SMT implementation in Rust built on taceo-poseidon2, so the tests confirm the Poseidon2 port against a second implementation rather than against itself.

Candidate 3 of 3 for credential revocation list support. Follows the
Iden3 / Polygon ID revocation model: leaves sit at their shortest
distinguishing depth, so the level count tracks the number of revoked
entries rather than the key width.

Vendors iden3's smt/ circuits (absent from this repo's partial circomlib
copy) with SMTHash1/SMTHash2 ported to Poseidon2, plus the missing
Switcher template. Both non-membership cases are covered: the path
terminating on an empty node and on a different leaf.

Not for merge — opened to measure constraint cost against the indexed
Merkle tree and full SMT candidates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant