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
- Payers sign Payment Mandates (EIP‑712 typed data) off‑chain.
- Payees forward mandates to the SP.
- The SP builds a batch, verifies signatures and policy checks inside a Groth16 circuit, aggregates payouts, and outputs a succinct proof.
- 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:
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):
ℓ_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)):
o_j = (payer_j, payee_j, token_j, sum_j)
sum_j = Σ amounts for the same (payer, payee, token)Commit with:
R_payouts = MerkleRoot(o_1..o_k)Public inputs are kept small to minimize gas:
R_mandates, R_null, R_payouts, t_batch, batchIdCircuit Overview (Groth16, BN254)
For all i:
- ECDSA correctness (non‑native secp256k1)
- Standard verification against EIP‑712 digest
z_i, replacingnon‑chainecrecovercalls with one proof.
- Standard verification against EIP‑712 digest
- Policy & sanity
deadline_i >= t_batch(public)0 < amount_i <= limit(payer_i)(constant or via inclusion proof)- Integrity: the Poseidon leaf
l_imatches decodedm_i
- Uniqueness / anti‑replay
- Compute
v_i = Poseidon(payer_i, nonce_i) - Sort
{v_i}in‑circuit and enforce adjacent inequality; expose nullifier rootR_null
- Compute
- Aggregation correctness
- Sum per
(payer, payee, token); expose payouts rootR_payouts
- Sum per
Public inputs:
pub = { R_mandates, R_null, R_payouts, t_batch, batchId }On‑Chain Contracts
ZKSettlementVerifier (stores verifying key)
function verifyAndSettle(
Proof proof,
PubInputs pub,
Payout[] calldata payouts, // (payer, payee, token, sum)
bytes32[] calldata nullifiers
) external;Flow:
- Recompute
R_payoutsandR_nullfrom calldata; check equality topub. - Verify proof via BN254 precompiles.
- For each nullifier: require unused; mark used.
- For each payout: call
DebitWallet.debitFrom(payer, token, payee, amount). - 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
- Ingestion: receive
(m_i, σ_i)from payees. - Pre‑checks: stateless sig check, balance hinting, dedup.
- Batch builder: choose
n; group payouts; computeR_mandates,R_payouts,R_null. - Prover: produce Groth16 proof with public inputs
pub. - 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):
non‑chain signature checks +ntransfers. - 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.
