Skip to content

ZK‑Verified Settlement Processor (Batch Aggregation)

Summary

An AEP2/ASP‑compatible Settlement Processor that proves mandate correctness off‑chain and settles many payouts with one on‑chain verification. It keeps the authorize‑first, settle‑later model, removes per‑transaction signature checks from chain, and batches transfers to cut gas and UX latency.

Roles (inherited from AEP2 / ASP)

  • Payer Agent & Debit Wallet (AA wallet): User‑funded contract; authorizes specific Settlement Processors (SPs); enforces a withdrawal delay to protect in‑flight settlements.
  • Payee Agent: Verifies mandates off‑chain and forwards them to the SP.
  • Settlement Processor (SP): Ingests mandates, validates them, builds batches, produces a Groth16/BN254 proof, and triggers on‑chain settlement.

High‑Level Batch Flow

  1. Payers sign Payment Mandates (EIP‑712 typed data) off‑chain.
  2. Payees forward mandates to the SP.
  3. The SP builds a batch, verifies signatures and policy checks inside a Groth16 circuit, aggregates payouts, and outputs a succinct proof.
  4. On chain, Verifier + DebitWallet verify the proof once (BN254 precompiles) and execute multi‑payout settlement in a single transaction.

Flow Diagram

┌──────────────────────────────┐      ┌──────────────────────────────┐
│  Payer Agent + Debit Wallet  │ ───► │          Payee Agent         │
└──────────────────────────────┘      └───────────────┬──────────────┘
                             Mandates (EIP‑712)       │
                                                      │  forward (batched)

                                           [m1] [m2] [m3] … [mn]


┌──────────────────────────────────┐      ┌─────────────────────────────┐
│ Settlement Processor (off‑chain) │ ───► │ On‑Chain: Verifier + Wallet │
└──────────────────────────────────┘      └──────────────┬──────────────┘
Batch: {m1..mn} → R_mandates, R_null, R_payouts          │  Submit:
Prove: Groth16 → Proof                                   │  Proof + Roots
                                                         │  + Payouts 
                                                         │  + Nullifiers

                                             Multi‑payout transfers
                                             debitFrom(...) per payout
┌──────────────────────────────┐       ┌──────────────────────────────┐
│ Off‑Chain Pipeline           │       │ On‑Chain Settlement          │
│ 1) Ingest (m_i, σ_i)         │       │ 1) Recompute roots           │
│ 2) Pre‑checks (sig, balance, │       │ 2) Verify proof (BN254)      │
│    dedup)                    │       │ 3) Mark nullifiers used      │
│ 3) Build batch:              │       │ 4) debitFrom(...) per payout │
│    R_mandates, R_null,       │       │ 5) Emit BatchSettled(...)    │
│    R_payouts                 │       └──────────────────────────────┘
│ 4) Prover → Groth16 proof    │
│ 5) Submit verifyAndSettle()  │
└──────────────────────────────┘

Why Groth16 on BN254 (EVM)?

Constant‑size proofs and efficient verification via bn254 precompiles (~180k–250k gas base + ~6k per public input, implementation‑dependent) are materially cheaper than verifying many signatures individually and fit deferred settlement.

Payment Mandate (EIP‑712)

  • Domain: name, version, chainId, verifyingContract
  • Types:
text
Mandate: { payer, payee, token, amount, nonce, deadline, salt }
  • Digest: keccak256("\x19\x01" || domainSeparator(D) || structHash(m))
  • Signature: ECDSA (secp256k1) over the EIP‑712 digest: (r, s, v)

Batch Commitments & Data Structures

For a batch B = {(m_i, σ_i)}_{i=1..n}:

  • Leaf commitment (SNARK‑friendly hash, e.g., Poseidon):
text
ℓ_i = Poseidon(payer_i, payee_i, token_i, amount_i, nonce_i, deadline_i)
  • Mandates root: R_mandates = MerkleRoot(l_1..l_n)
  • Nullifier (anti‑replay): v_i = Poseidon(payer_i, nonce_i) — must be fresh on‑chain
  • Aggregated payouts (group by (payer, payee, token)):
text
o_j = (payer_j, payee_j, token_j, sum_j)
sum_j = Σ amounts for the same (payer, payee, token)

Commit with:

text
R_payouts = MerkleRoot(o_1..o_k)

Public inputs are kept small to minimize gas:

text
R_mandates, R_null, R_payouts, t_batch, batchId

Circuit Overview (Groth16, BN254)

For all i:

  1. ECDSA correctness (non‑native secp256k1)
    • Standard verification against EIP‑712 digest z_i, replacing n on‑chain ecrecover calls with one proof.
  2. Policy & sanity
    • deadline_i >= t_batch (public)
    • 0 < amount_i <= limit(payer_i) (constant or via inclusion proof)
    • Integrity: the Poseidon leaf l_i matches decoded m_i
  3. Uniqueness / anti‑replay
    • Compute v_i = Poseidon(payer_i, nonce_i)
    • Sort {v_i} in‑circuit and enforce adjacent inequality; expose nullifier root R_null
  4. Aggregation correctness
    • Sum per (payer, payee, token); expose payouts root R_payouts

Public inputs:

text
pub = { R_mandates, R_null, R_payouts, t_batch, batchId }

On‑Chain Contracts

ZKSettlementVerifier (stores verifying key)

solidity
function verifyAndSettle(
  Proof proof,
  PubInputs pub,
  Payout[] calldata payouts,      // (payer, payee, token, sum)
  bytes32[] calldata nullifiers
) external;

Flow:

  1. Recompute R_payouts and R_null from calldata; check equality to pub.
  2. Verify proof via BN254 precompiles.
  3. For each nullifier: require unused; mark used.
  4. For each payout: call DebitWallet.debitFrom(payer, token, payee, amount).
  5. Emit BatchSettled(batchId, hash(payouts), nMandates).

AgentDebitWallet (AA, per AEP2/ASP)

Tracks balance, queueBalance, available = balance - queueBalance, authorized SPs, and a withdrawal delay (auto‑extends when new mandates arrive). debitFrom(...) is callable only by an authorized SP/verifier; ensures sufficient available (or uses reservation accounting) before ERC‑20 transfer.

Data Availability

Full batch contents can be published off‑chain (e.g., IPFS/Blob). On chain we store Merkle roots; BatchSettled can reference a CID so anyone can reconstruct and audit.

Off‑Chain SP Pipeline

  1. Ingestion: receive (m_i, σ_i) from payees.
  2. Pre‑checks: stateless sig check, balance hinting, dedup.
  3. Batch builder: choose n; group payouts; compute R_mandates, R_payouts, R_null.
  4. Prover: produce Groth16 proof with public inputs pub.
  5. Submit: call verifyAndSettle(proof, pub, payouts, nullifiers).

Correctness & Security

  • Soundness: a valid proof implies all EIP‑712 signatures are valid at t_batch, and grouped sums are correct.
  • No replay: nullifiers enforced distinct in‑circuit and marked used on chain.
  • Funds safety: DebitWallet withdrawal delay + queue accounting protect payees; SP must be pre‑authorized per wallet.
  • Auditability: on‑chain roots bind to off‑chain data; any tampering breaks membership proofs.

Gas & Cost (order‑of‑magnitude)

  • Baseline (no ZK): n on‑chain signature checks + n transfers.
  • With ZK aggregation: one Groth16 verify (~181k + 6k·|pub| gas, ballpark) + transfers (often fewer thanks to payout grouping).

On L2s, relative savings increase due to calldata/compute pricing.

Observation: After aggregation, signature cost is amortized; transfers dominate when many unique payees exist. Where business rules permit, netting and grouping further reduce cost.

Interop & Variants

  • Drop‑in compatible: works alongside non‑ZK Settlement Processors.
  • Policy commitments: include payer limits/compliance attestations as Merkle commitments with inclusion proofs.
  • Dispute hooks: optional guards to pause or claw back specific payout leaves by (batchId, payoutIndex).
  • Alternate proofs: Groth16/BN254 favored for EVM gas; plonkish/STARK variants trade setup/DA ergonomics vs. higher on‑chain verify cost.

Released under the MIT License.