Specification · Version 1.0.0 · 2026-04-21

Commons Cryptographic Protocol

The full cryptographic construction of Commons. Every proof, hash, and domain separator an auditor needs to reproduce our claims.

UltraHonk over BN254 Aztec Ignition SRS, 1-of-N honesty 0 live circuits three-tree membership · position note · debate weight · bubble membership

Canonical source CRYPTOGRAPHY-SPEC.md
01

Trust stack

Four layers. The integrity ceiling is set by the weakest.

  1. L4 ZK Proof Verification trustless

    UltraHonk on BN254 provides computational soundness under the algebraic group model and the hardness of discrete log on BN254. If a proof verifies, the prover demonstrably knows a witness satisfying every circuit constraint. No off-chain component is trusted.

  2. L3 Root Registries observable

    Three immutable append-only registries on Scroll L2. Lifecycle transitions require 7-day timelocks; verifier upgrades require 14 days. The guarantee is transparency with exit rights — malicious governance action is visible on-chain before execution.

  3. L2 Tree Construction trusted

    The Shadow Atlas operator downloads public Census TIGER data and builds Poseidon2 Merkle trees. The operator cannot forge proofs (user secrets are client-side only) but can poison the tree (mis-map an address) or censor (omit a user). Mitigations documented; walkaway roadmap published.

  4. L1 Data Acquisition verifiable

    Census TIGER/Line boundary data is public, free, and published with SHA-256 checksums. Anyone can download the same shapefiles and verify them. The trust assumption is that the Census Bureau publishes accurate boundaries.

02

Primitives

Three primitives, nested. A prime field; a hash that operates on it; a sponge that chains the hash.

Field

BN254 scalar

p ≈ 2.188 × 1076 order of magnitude
254 bits

p =

21888242871839275222246405745257275088548364400416034343698204186575808495617

All field elements are members of Fp. Every external input is validated < p before circuit execution.

Hash, over Fp

Poseidon2

In-circuit constraint cost per hash
Poseidon2 ~160
SHA-256 ~25,000

~156× cheaper in-circuit. Field-native; no bit decomposition.

t
4
rate
3
capacity
1
RF
8
RP
56

Security level 128. Implementation: Noir stdlib. Parameters match the reference Aztec / ZCash specification.

Sponge, over Poseidon2

24-input absorption

Poseidon2 sponge constructionstateccapacityr₁rater₂rater₃rateSONGE_24domain init+=+=+=inputsinputs[3i]inputs[3i+1]inputs[3i+2]Poseidon2_permute(state)loop×8after the 8th permutationreturn state[0] = H(inputs)
Poseidon2 sponge absorption. The state has four slots: one capacity slot initialized with the domain tag SONGE_24, and three rate slots. Each of eight iterations adds three inputs into the rate slots using plus-equals (not assignment), then applies the Poseidon2 permutation. The capacity slot carries the cryptographic chain across all iterations. After the final permutation, state[0] is returned as the hash output over all 24 inputs.

The += is load-bearing. Overwriting rate slots would break the cryptographic chain — each permutation output must carry forward into the next absorb step. A cross-language golden-vector check runs in CI: Noir, TypeScript, and Rust implementations must produce bit-identical digests.

03

Domain separation

Every hash output carries a tag at a fixed position in the Poseidon2 state. Seven tags define the protocol’s typology. No tag can change without invalidating all historical proofs.

Arity separation

One tag per input count. The tag’s position shifts right as arity grows — visible across the cluster.

"H1M"

= 0x48314d

  1. x
  2. H1M
  3. 0
  4. 0
arity
1
use
Single-input hash

"H2M"

= 0x48324d

  1. a
  2. b
  3. H2M
  4. 0
arity
2
use
Merkle nodes, nullifier, cell-map leaf, engagement leaf

"H3M"

= 0x48334d

  1. a
  2. b
  3. c
  4. H3M
arity
3
use
Engagement data commitment, debate note commitment

"H4M"

= 0x48344d

2-round sponge — state width exceeded, tag carried across permutations

arity
4
use
User leaf: H4(user_secret, cell_id, registration_salt, authority_level)

Purpose separation

Same arity, different semantic domain. PCM and PNL both consume three inputs into the final slot; distinct tag values prevent cross-purpose collisions.

"PCM"

= 0x50434d

  1. arg
  2. wt
  3. rand
  4. PCM
arity
3
use
Debate position commitment

"PNL"

= 0x504e4c

  1. key
  2. c
  3. dbt
  4. PNL
arity
3
use
Debate position nullifier

Capacity initialization

Tag seeds the capacity slot, making the entire sponge computation depend on the domain from its first permutation.

"SONGE_$"

= 0x534f4e47455f24

  1. SONGE_$
  2. 0
  3. 0
  4. 0
arity
24
use
District commitment · sponge capacity seed · mnemonic SONGE_24
04

Data structures

Four Merkle trees carry the protocol state. Three persistent trees link through shared witnesses; one is debate-scoped and cryptographically isolated.

Four-tree data topologyTree 2sparse · geographydepth 20cell_idTree 1binary · identitydepth 18–24hubidentity_commitmentTree 3binary · engagementdepth 20Position Treebinary · per-debatedepth 20isolated · pruned on settlecore · persistent, linked
Four-tree data topology. Tree 1 is the identity hub. Tree 2 (sparse Merkle, geographic) attaches to Tree 1 via the shared witness cell_id. Tree 3 (binary Merkle, engagement) attaches to Tree 1 via identity_commitment. Position Tree is cryptographically isolated: each debate has its own tree, pruned on settle.

Core · persistent, linked

Tree 1 — Identity

binary Merkle

Leaf

H4(user_secret, cell_id, registration_salt, authority_level)
depth
18 / 20 / 22 / 24  · scales with population tier
node
H2(left, right)
lifecycle
stable — user re-registers only on physical move

cross-tree cell_id binds to Tree 2; user_secret derives the identity_commitment that feeds Tree 3 and the nullifier (§5). authority_level bound in leaf (BR5-001).

Tree 2 — Cell→Districts

sparse Merkle, key-derived

Leaf

H2(cell_id, district_commitment)
depth
20
node
H2(left, right)
lifecycle
dynamic — redistricts on cycle; Tree 1 identities unaffected

cross-tree cell_id is the join key with Tree 1. Sparse path means proof size is logarithmic in occupied keys, not in depth.

Tree 3 — Engagement

binary Merkle

Leaf

H2(ic, H3(tier, action_count, diversity_score))
depth
20
node
H2(left, right)
lifecycle
append-only — updated per verified civic action

cross-tree ic is the same private identity_commitment that feeds the nullifier. The cross-tree binding in §5 enforces that no engagement leaf can be claimed by a foreign identity.

Debate-scoped · ephemeral

Position Tree

binary Merkle

Leaf

H_PCM(argument_index, weighted_amount, randomness)
depth
20
node
H2(left, right)
lifecycle
per debate — pruned on settle

cross-tree Leaf uses the PCM domain tag (§3), not H3M — preventing cross-circuit commitment aliasing with engagement data. Consumed by position_note circuit (§6).

05

Cross-tree identity binding

A single private input feeds two derivations.

Cross-tree identity bindingH2(  ic , action_domain )H2(  ic , engagement_data_commitment )ic ≡ iccircuit-enforced equalityidentity_commitmentprivate · deterministic per verified personnullifierpublic · on-chainprevents double-submissionengagement_leafmember of Tree 3binds engagement data to identity
Cross-tree identity binding: a private identity_commitment value feeds both the nullifier derivation (H2 with action_domain) and the engagement leaf derivation (H2 with engagement_data_commitment). The circuit enforces that the same ic is used in both derivations — the equality ic ≡ ic is the cryptographic joint preventing cross-identity claims.

Zero-knowledge

The proof reveals none of ic. An observer sees nullifier and engagement_leaf, both one-way derived. Recovering ic reduces to breaking the discrete-log assumption on BN254.

Equality assertion

The circuit asserts the ic consumed by the nullifier derivation equals the ic consumed by the leaf derivation. Rotating between them fails at witness generation — no satisfying proof exists.

Attacker cannot extract ic, and cannot substitute a different one. Engagement data cannot be claimed by a foreign identity.

06

Circuits

Private · stays with youuser_secret↖ private witnessidentity_commitmentH2hashproof equationPublic · anyone can verifyuser_root↙ public receiptproofprover →→ verifier

This grammar repeats across every circuit. A private witness enters a hash inside the prover, producing a value that the proof equation binds to a public receipt on-chain.

Private · your leaf enters herePublic · the known rootleaf↖ leaf · the hash of your private dataHsib₀← sibling · another hash at this levelHsib₁← ladder · each step = H(current, sibling)Hsib₂real circuits climb 20 levelsroot↓ root · on-chain commitment to the whole set

When you must prove membership in a known set, the grammar extends. Your value hashes into a leaf. Each climb combines it with a sibling — another hash at that tree level — until the ladder of twenty climbs reaches the root, the set's on-chain commitment. The chain of siblings is the proof.

Civic action

three_tree_membership

identity ⟶ action

I'm a verified person in one of these 24 districts, at engagement tier ≥ N, and this is my one action.

Private · stays with you

  • identity witnesses
  • exact H3 cell
  • action history

Public · anyone can verify

  • 24-district set
  • tier threshold
  • one-time action receipt
  • authority
Tree 1 · user identity

Identity proves this is a real, uniquely registered person. The deterministic output is the identity_commitment — abbreviated ic — a stable per-person value that never reveals who.

user_root ≡ merkle( H4(user_secret, cell_id, registration_salt, authority_level), path, idx )

Tree 2 · cell → districts

Location proves the person lives in one of the 24 accepted districts.

cell_map_root ≡ smt( H2(cell_id, sponge₂₄(districts)), path, bits )

Cross-tree binding · ic forks see §5

Identity and location bind to this one action — and only this one. The same ic from ① forks into a nullifier (prevents double-spend) and into the engagement record.

nullifier ≡ H2(ic, action_domain)  ·  engagement_root ≡ merkle( H2(ic, H3(tier, count, diversity)), path, idx )

bubble_membership

identity ⟶ community field

I'm a verified person who lives in these specific map cells.

Private · stays with you

  • identity
  • individual cell IDs
  • engagement details

Public · anyone can verify

  • cell_set_root
  • cell_count
  • epoch_nullifier
Cross-tree binding · ic forks see §5 · same shape as 3TM ③

engagement_root ≡ merkle₂₀( H2(ic, H3(tier, count, div)), path, idx )  ·  epoch_nullifier = H2(ic, epoch_domain)

Cell set · sorted · merkle₄

cell_set_root ≡ merkle₄( cell_ids[16] )  ·  sorted, zero-padded, MAX_CELLS = 16

Debate market

debate_weight

stake ⟶ weight

My influence is √stake × 2^tier — without revealing stake or tier.

Private · stays with you

  • stake
  • tier
  • randomness

Public · anyone can verify

  • weighted_amount
  • note_commitment
Sqrt bound · stake trapped between consecutive squares u64 arithmetic

sqrt_stake² ≤ stake < (sqrt_stake + 1)²  ·  the interval traps exactly one integer root

Quadratic weighting · pedestal × ladder

weighted_amount ≡ sqrt_stake × 2^tier

Note commitment · handoff H3M domain · §3

note_commitment ≡ H3(stake, tier, randomness)  ·  consumed by position_note

position_note

commitment ⟶ settlement

I own a position in the winning argument and I'm claiming its payout.

Private · stays with you

  • which position
  • stake
  • tier
  • randomness
  • identity

Public · anyone can verify

  • position_root
  • nullifier
  • debate_id
  • winning argument
  • payout
Position commitment PCM domain · §3

commitment = H_PCM( argument_index, weighted_amount, randomness )

Tree membership · merkle₂₀

position_root ≡ merkle₂₀( commitment, position_path, position_index )

Settlement identities · prover cannot substitute

argument_index ≡ winning_argument_index  ·  weighted_amount ≡ claimed_weighted_amount

Position nullifier · prevents double-claim PNL domain · §3

nullifier ≡ H_PNL( nullifier_key, commitment, debate_id )  ·  pre: nullifier_key ≠ 0

07

Nullifier scheme

A natural first cut: hash a per-registration secret and call it a nullifier. But nothing binds that secret to a person — register again with a new secret, the hash changes, one person gets two nullifiers for the same action. Binding the nullifier to a verified identity instead closes the attack before it opens.

Naïve · Sybil via re-registration one person, two keys → two nullifiers for the same action

user_secret varies with each registration. Two secrets → two hash outputs → two valid nullifiers for the same action_domain. The attacker casts two votes as one person.

Adopted · anchored to identity one person, any keys → one nullifier per action

H_id is deterministic on the mDL's stable fields. Same person, same identity_commitment — regardless of user_secret, registration_salt, or cell_id. Feeding it into H2 with a contract-fixed action_domain collapses all re-registrations onto the same output.

Formal statement

naïve nullifier = H2(user_secret, action_domain) Sybil via re-registration
adopted nullifier = H2(identity_commitment, action_domain) deterministic per verified person
identity_commitment private
derivation H_id(mDL signed fields)
invariance stable across user_secret, registration_salt, cell_id
trust anchor mDL issuer signature (state DMV)
action_domain public
derivation keccak256(protocol, country, jurisdictionType, recipientSubdivision, templateId, sessionId) mod BN254
invariance one nullifier per (user, recipient, template, legislative session)
trust anchor DistrictGate.allowedActionDomains — governance-whitelisted
08

Trusted setup

The protocol's zero-knowledge machinery rests on a public reference string. If the party who generated it kept a secret, every proof could be forged. Here's how that didn't happen.

0 participants · Aztec Ignition ceremony, Jan 2020 · each added randomness, each destroyed their copy
this one · or any other
If one of these 176 contributors destroyed their copy, the SRS is secure. To forge: all 176 must collude and all must have kept their secrets.
What it is
Proving system UltraHonk + KZG commitments
SRS ceremony Aztec Ignition · Jan 2020 · BN254
Per-circuit setup none required
Why it's safe
Participants 0
Honesty threshold 1-of-176
Public audit transcript + participants
09

Known limitations

Load-bearing for the walkaway roadmap. None block shipping. Ordered worst-first.

Open gaps

live threats to the security model
  • Operator tree-construction trust

    The Shadow Atlas operator can poison or censor tree construction. Walkaway roadmap target. See TRUST-MODEL-AND-OPERATOR-INTEGRITY.md §5.

  • Reproducible build pipeline

    A cryptographer cannot today reproduce the verifier contract bytecode from the nargo source without insider knowledge of toolchain versions and flags. This undermines the 14-day verifier upgrade timelock's community-verification property. Remediation: pin toolchain in Docker / Nix.

Design trade-offs

intentional, with stated mitigation
  • Immediate root registration

    New roots are registered without timelock (fast UX). A compromised governance key can register a poisoned root instantly; the poisoning must still pass Census-based independent verification.

Planned work

absence of evidence, not active risk
  • Professional security audit

    Three internal review waves have occurred. Findings documented in docs/wave-4xR-*-review.md. No external firm has audited the circuits or contracts.

  • Formal verification

    No circuit is currently formally verified. Domain-separation non-collision is asserted by property tests, not by a formal proof.