PLONK Membership Proofs
How PLONK proofs over BLS12-381 provide 128-bit security for ZK membership verification without a per-circuit trusted setup.
PLONK Membership Proofs
Why PLONK?
UPC uses PLONK (Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of Knowledge) as its proof system, rather than Groth16 or STARKs:
| Property | Groth16 | PLONK | STARK |
|---|---|---|---|
| Trusted setup | Per-circuit | Universal (once) | None |
| Proof size | ~200B | ~400B | ~5KB |
| Verification gas | ~230K | ~400K | ~20M |
| Post-quantum | No | No | Yes |
| Curve | BN254 | BLS12-381 | N/A |
PLONK's key advantage for compliance: the universal trusted setup. A single Powers of Tau ceremony covers all PLONK circuits. No per-circuit "toxic waste" ceremony needed. This removes an entire category of audit question: "Who ran the setup and can we trust them?"
Why BLS12-381?
UPC uses BLS12-381 (not BN254) for 128-bit security:
| Property | BN254 | BLS12-381 |
|---|---|---|
| Security level | ~100-bit | ~128-bit |
| EVM support | Native precompile | EIP-2537 (Pectra, May 2025) |
| Pairing gas | ~100K | ~500K |
EIP-2537 (Pectra)
BLS12-381 precompiles went live on Ethereum mainnet with the Pectra hard fork in May 2025. PLONK verification over BLS12-381 is now gas-efficient on L1.
Institutions requiring 128-bit security (NIST post-2030 recommendations) could not use BN254. BLS12-381 satisfies this requirement.
The Membership Circuit
The Circom membership circuit proves:
Public inputs:
root - Merkle root of the approved list
nullifier - Poseidon(identity, nonce) - prevents proof reuse
Private inputs:
identity - Poseidon(address) - identity commitment
merkleProof - Path from leaf to root
address - The actual address
nonce - Anti-replay counter
Constraints:
Poseidon(address) == identity
MerkleVerify(identity, merkleProof) == root
Poseidon(identity, nonce) == nullifierThe verifier learns: the prover is in the tree, and has a unique nullifier. Nothing else.
Pluggable Hash Functions
The membership tree supports two hash functions via IHashFunction:
| Implementation | Security | When to Use |
|---|---|---|
PoseidonBLS12381 | 128-bit | Default — use with PLONK on BLS12-381 |
PoseidonBN254 | 100-bit | Legacy — use with Groth16/PLONK on BN254 |
import { PoseidonBLS12381 } from '@permissionless-technologies/upc-sdk/core'
const asp = createASPClient({
hashFunction: new PoseidonBLS12381(), // default
// ...
})On-Chain Verification
interface IAttestationVerifier {
/// @notice Verify a ZK membership proof
/// @param proof The PLONK proof bytes
/// @param publicInputs [root, nullifier]
/// @return true if proof is valid
function verify(
bytes calldata proof,
uint256[] calldata publicInputs
) external view returns (bool);
}The AttestationHub maintains a registry of IAttestationVerifier implementations. Protocols query the registry to verify against any registered ASP.
Proof Reuse Prevention
Each proof includes a nullifier computed from the identity and a nonce:
nullifier = Poseidon(identity, nonce)The on-chain verifier tracks used nullifiers. This prevents:
- Reusing the same proof in multiple transactions
- Transferring a proof to another user
Nullifiers are optional for read-only verification (balance checks, eligibility queries) and required for state-changing operations (deposits, withdrawals).