Domain Management & Aggregation

What happens after your proof is verified

Your proof is on-chain. The verifier pallet accepted it. A statement hash was recorded. Now what?

If you submitted with a domainId, your proof enters the aggregation engine. This is where verified proofs from many applications get batched together, their attestations compressed into a single Merkle root, and that root published to a destination chain your smart contract can read.

This lesson covers what domains are, how aggregation works, and exactly what lands on the destination chain.

What a domain is

A domain is a logical container for aggregating proof statements. Every domain has three defining properties:

Aggregation size — how many verified proof statements are collected before the Merkle root is published. A power of two is recommended since the aggregation is a Merkle tree. Smaller means more frequent publication but higher per-proof bridging cost. Larger means cheaper bridging but longer wait times.

Queue size — how many completed aggregations can wait in a buffer before being published. Maximum 16. If the queue fills, new proof submissions to that domain fail with a CannotAggregate(DomainFull) error until at least one pending aggregation is published.

Delivery destination — where the Merkle root gets published. Either a specific chain (via Hyperbridge or a relayer bot) or None if you only need the on-chain attestation without cross-chain settlement.

For most developers, you do not register your own domain. You pick an existing domain that routes to your target chain and pass its domainId when submitting proofs. The zkVerify documentation maintains a list of active domains with their target chains and aggregation sizes.

How aggregation works step by step

`` Proof verified on zkVerify ↓ Statement hash recorded as a leaf ↓ Domain collects leaves until aggregation size is reached ↓ (or a timeout triggers early publication) Merkle tree built from all leaves in the batch ↓ NewAggregationReceipt event emitted — contains the root, domainId, and aggregationId ↓ Relayer publishes the root to the destination chain's zkVerify smart contract ↓ AggregationPosted event emitted on destination chain ``

The aggregation is a digitally signed message containing the Merkle root. It is not a ZK proof. It is a cryptographic commitment that lets any smart contract verify Merkle inclusion cheaply.

Published aggregations have a limited lifetime on zkVerify. An aggregation remains in the published storage for only the block in which it was published. If you need the Merkle path for your proof, you must retrieve it at exactly the block where the NewAggregationReceipt event was emitted. The statement hash from your proof submission is what you use to look it up.

Retrieving your Merkle path

Once the NewAggregationReceipt event fires, use getAggregateStatementPath to fetch your proof's position in the Merkle tree:

```typescript session.subscribe([ { event: ZkVerifyEvents.NewAggregationReceipt, callback: async (eventData) => { const statementPath = await session.getAggregateStatementPath( eventData.blockHash, parseInt(eventData.data.domainId), parseInt(eventData.data.aggregationId), statement // your statement hash from proof submission );

fs.writeFileSync( "aggregation.json", JSON.stringify({ ...statementPath, domainId: parseInt(eventData.data.domainId), aggregationId: parseInt(eventData.data.aggregationId) }) ); }, options: { domainId: 0 } } ]); ```

The saved aggregation.json will look like this:

``json { "root": "0xef4752160e8d7ccbc254a87f71256990f2fcd8173e15a592f7ccc7e130aa5ab0", "proof": [ "0x40fbf21f1990ef8d1425d12ec550176fe848a7c63f0c59f7a48101e51c9aceee", "0x0be311c3643fb3fcd2b59bf4cfd02bdef943caf78f92d94a080659468c38fef9" ], "numberOfLeaves": 8, "leafIndex": 0, "leaf": "0xc5a8389b231522aad8360d940eb3ce275f0446bba1a9bd188b31d1c7dd37f136", "domainId": 0, "aggregationId": 137 } ``

This is everything your smart contract needs to verify that your proof was included in a published aggregation.

The settlement smart contract

zkVerify deploys a verification smart contract on each supported destination chain. The contract stores a mapping of domainId to aggregationId to Merkle root:

```solidity // What the contract stores mapping(uint256 => mapping(uint256 => bytes32)) public proofsAggregations;

// What you call to verify your proof was included function verifyProofAggregation( uint256 _domainId, uint256 _aggregationId, bytes32 _leaf, bytes32[] calldata _merklePath, uint256 _leafCount, uint256 _index ) external view returns (bool) ```

Your application contract calls verifyProofAggregation with the data from aggregation.json. If it returns true, your proof was verified by zkVerify and included in the published aggregation. You can safely act on the result.

The destination chain never sees the original proof. It only processes a Merkle path check — a cheap storage read and hash comparison regardless of what the original proof type was.

Cost sharing

Aggregation costs are shared across all proof submitters in a batch proportionally. When you submit a proof to a domain, you are implicitly splitting the bridging cost with everyone else in that aggregation.

Whoever calls the aggregate() extrinsic to publish an aggregation is refunded the transaction cost and receives a small additional tip. This makes aggregation permissionless and economically self-sustaining — anyone can trigger it and earn the fee.

Aggregation without settlement

If you do not need the Merkle root published on another chain, you can omit domainId from your proof submission or use a domain configured with destination: None. The proof is still verified and recorded on zkVerify. The statement hash is still available. You simply do not get cross-chain settlement.

This is appropriate for applications that only need on-chain attestation on zkVerify itself, or that handle cross-chain messaging through their own infrastructure.

Answer the quiz correctly to continue →

Quiz · Multiple Choice1 / 3

What is the purpose of aggregation in zkVerify?