Context
Proving is Circom-only today; the Noir crates (noir/crates/dg1, noir/crates/econtent) do not yet implement the nullifier/scope/commitment hashes, and the Noir migration is active. This proposes a hardening to build into Noir by construction ,explicitly not a patch to deployed Circom.
The gap
Three Poseidon usages lack per-purpose domain separation:
- Nullifier —
Poseidon(2)([secret, scope]) (vc_and_disclose.circom:133, vc_and_disclose_kyc.circom:135)
- KYC leaf —
Poseidon(2)([secret, msg_hasher.out]) (vc_and_disclose_kyc.circom:105)
- Commitment —
Poseidon(5)([secret, attestation_id, ...]) (register.circom:218-224)
Nullifier and KYC leaf are both arity-2 with secret first, differing only in operand 2. No tag distinguishes "nullifier" from "tree leaf".
Severity
Defense-in-depth aligned with Semaphore v4 practice not a demonstrated vulnerability. scope is verifier-bound and enforced on-chain (IdentityVerificationHubImplV2.sol:833-844) and off-chain (SelfBackendVerifier.ts:62,106-115); there is no known path to force scope == msg_hasher.out. Value: don't let the Noir port inherit the untagged construction.
Why not patch Circom
Tagging changes hash outputs: stored nullifiers (IdentityRegistryImplV1.sol:83) stop matching → double-use regression; commitment change invalidates the on-chain identity tree; and any R1CS change forces a new Groth16 phase-2 ceremony plus verifier redeployment. The hardening is only free where there is no deployed state ,the unbuilt Noir circuits.
Proposal
- Implement Noir nullifier/scope/commitment with explicit tag constants from day one (
H(TAG_NULLIFIER, ...), H(TAG_LEAF, ...), H(TAG_COMMITMENT, ...)).
- Ship divergence vectors (legacy Circom as reference) enumerating exactly which outputs differ and why equivalence is intentionally not a goal.
- Include a compatibility analysis: nullifier-set invalidation, tree re-registration, trusted-setup impact, and a circuit-version coexistence strategy.
Out of scope
Deployed Circom/contracts/registries; scope derivation (correct as-is); any exploit claim.
Questions for maintainers
- Is Noir the long-term proving path, and is now the right time to bake this in?
- Preferred domain-tag scheme?
- Version coexistence vs. flag-day cutover?
cc @Nesopie @remicolin @0xturboblitz
Context
Proving is Circom-only today; the Noir crates (
noir/crates/dg1,noir/crates/econtent) do not yet implement the nullifier/scope/commitment hashes, and the Noir migration is active. This proposes a hardening to build into Noir by construction ,explicitly not a patch to deployed Circom.The gap
Three Poseidon usages lack per-purpose domain separation:
Poseidon(2)([secret, scope])(vc_and_disclose.circom:133,vc_and_disclose_kyc.circom:135)Poseidon(2)([secret, msg_hasher.out])(vc_and_disclose_kyc.circom:105)Poseidon(5)([secret, attestation_id, ...])(register.circom:218-224)Nullifier and KYC leaf are both arity-2 with
secretfirst, differing only in operand 2. No tag distinguishes "nullifier" from "tree leaf".Severity
Defense-in-depth aligned with Semaphore v4 practice not a demonstrated vulnerability.
scopeis verifier-bound and enforced on-chain (IdentityVerificationHubImplV2.sol:833-844) and off-chain (SelfBackendVerifier.ts:62,106-115); there is no known path to forcescope == msg_hasher.out. Value: don't let the Noir port inherit the untagged construction.Why not patch Circom
Tagging changes hash outputs: stored nullifiers (
IdentityRegistryImplV1.sol:83) stop matching → double-use regression; commitment change invalidates the on-chain identity tree; and any R1CS change forces a new Groth16 phase-2 ceremony plus verifier redeployment. The hardening is only free where there is no deployed state ,the unbuilt Noir circuits.Proposal
H(TAG_NULLIFIER, ...),H(TAG_LEAF, ...),H(TAG_COMMITMENT, ...)).Out of scope
Deployed Circom/contracts/registries;
scopederivation (correct as-is); any exploit claim.Questions for maintainers
cc @Nesopie @remicolin @0xturboblitz