ZK Matching Verification
ZK Matching Verification
The core security and privacy breakthrough of SnarkSide lies in its ability to verify perpetual futures matches cryptographically, not operationally. Unlike traditional DEXs that rely on observable trade logs or signed user transactions, SnarkSide settles trades based solely on SNARK proofs — concise, non-interactive zero-knowledge attestations that two encrypted intents have matched under strict protocol constraints.
This system allows SnarkSide to operate as a dark, intent-based exchange with no public orderbooks, no visible trade flows, and no wallet-level traceability, while still maintaining strong on-chain settlement guarantees. This page details the proof construction flow and how match execution is validated on-chain using a dedicated Groth16 SNARK verifier.
Overview: The Matching Verification Lifecycle
Two valid intents are privately matched off-chain using an MPC handshake.
A match circuit is instantiated, which jointly verifies that the trade parameters satisfy protocol constraints.
A SNARK proof is generated attesting to the validity of the match.
The match_commitment and proof are submitted to the settlement contract.
The on-chain verifier contract:
Validates the SNARK against the hardcoded verification key.
Confirms both nullifiers are unused.
Transfers funds atomically between vaults.
Logs the match event and commits nullifiers to global state.
No trade metadata is ever published on-chain. Only what’s proven — never what’s observed.
Proof Construction Flow
Let’s break down the full sequence of steps taken to generate a valid match proof in SnarkSide.
Step 1: Intent Verification
Each relayer begins by verifying the incoming intents individually using the intent constraint circuit (covered earlier). This ensures:
Valid notional size
Permissible leverage and slippage
Fresh nullifier
Proper Poseidon-encoded intent commitment
Each intent is thus known to be internally valid before attempting any match.
Step 2: Constraint Satisfaction
Two relayers (holding intent A and intent B) jointly execute an off-chain match constraint circuit, checking:
A.side != B.side(opposite directions)abs(A.notional - B.notional) <= max(ε)slippage ranges of A and B overlapA.expiryandB.expiryare both in the futurenullifier_A ≠ nullifier_B
This is performed via an MPC session or local simulation (depending on the relayer setup).
If the conditions are met, the relayers jointly proceed to proof construction.
Step 3: Match Commitment Generation
A canonical match_commitment is computed as:
match_commitment = Poseidon(intent_commitment_A, intent_commitment_B, epoch_salt)This ensures:
Uniqueness: no two identical intent pairs will hash to the same commitment unless both A and B are byte-identical.
Ordering Invariance: Poseidon input sorting enforces
min(A,B), max(A,B)to prevent commitment manipulation.Salted Epoch Context: Epoch salt ensures temporal binding, allowing batch verification in shared settlement intervals.
Step 4: Match Circuit Execution
The ZK Match Circuit is written in Circom and defines all rules required for a valid perp match.
Simplified sketch:
template MatchCircuit() {
signal input commitment_A;
signal input commitment_B;
signal input nullifier_A;
signal input nullifier_B;
signal input margin_commitment_A;
signal input margin_commitment_B;
signal input epoch_salt;
signal output match_commitment;
// Enforce uniqueness of nullifiers
assert(nullifier_A ≠ nullifier_B);
// Reconstruct match commitment
component hash = Poseidon(3);
hash.inputs[0] <== min(commitment_A, commitment_B);
hash.inputs[1] <== max(commitment_A, commitment_B);
hash.inputs[2] <== epoch_salt;
match_commitment <== hash.output;
}The full circuit also includes:
Checks that both intent proofs are valid (can be nested via recursive SNARKs)
Verifications that margin_commitments are consistent with vault state roots
Output of nullifiers to prevent replay
The final output is:
match_commitment: stored on-chain as the trade identifierproof: Groth16 or PLONK artifact, typically ~192 bytespublic_inputs: includes nullifiers, commitments, and epoch metadata
SNARK-Verified Match Execution
The settlement phase happens on-chain and is fully proof-driven.
On-Chain Settlement Contract
SnarkSide’s MatchExecutor.sol contract exposes a method:
function settleMatch(
Proof calldata proof,
uint256[] calldata publicInputs
) external {
require(verifier.verifyProof(proof, publicInputs), "Invalid match proof");
bytes32 nullifierA = publicInputs[0];
bytes32 nullifierB = publicInputs[1];
// Check for replay
require(!nullifierUsed[nullifierA], "Nullifier A already used");
require(!nullifierUsed[nullifierB], "Nullifier B already used");
// Atomically update state
vault.update(...);
emit MatchSettled(matchCommitment, nullifierA, nullifierB);
// Mark nullifiers
nullifierUsed[nullifierA] = true;
nullifierUsed[nullifierB] = true;
}Verifier Module
The verifier contract uses a hardcoded vk.sol verifier artifact (generated from Circom). This ensures:
Matching logic is trustlessly enforced
Off-chain relayers cannot submit invalid trades
User funds in vaults are only moved by proof-verified interactions
Finality and State Mutation
Upon successful verification:
Both nullifiers are committed to global storage (non-replayable)
Funds are unlocked and transferred between margin vaults
Optional liquidation flags or funding adjustments are applied
Events are emitted for indexing (but contain no sensitive data)
No trade amount, direction, or wallet address is ever posted or required.
Summary
The ZK Matching Verification system replaces traditional trade settlement with cryptographic proof attestation. This results in:
Invisible execution: no front-running, no orderbook sniping
Absolute finality: once settled, trades cannot be reversed or replayed
Decentralized enforcement: relayers need no custody or control
Composable security: matches can be embedded into rollups, intents, or recursive proof stacks
In SnarkSide, the proof is the trade — and the chain is only responsible for validating it.
Last updated

