UPC — Universal Private ComplianceConcepts

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:

PropertyGroth16PLONKSTARK
Trusted setupPer-circuitUniversal (once)None
Proof size~200B~400B~5KB
Verification gas~230K~400K~20M
Post-quantumNoNoYes
CurveBN254BLS12-381N/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:

PropertyBN254BLS12-381
Security level~100-bit~128-bit
EVM supportNative precompileEIP-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) == nullifier

The 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:

ImplementationSecurityWhen to Use
PoseidonBLS12381128-bitDefault — use with PLONK on BLS12-381
PoseidonBN254100-bitLegacy — 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).

On this page