Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d897a09
feat(pool): add MARKPool ZK UTXO pool domain
iap May 12, 2026
02c9da0
fix(pool): fix PoolErrors, domain separators, remove dead code
iap May 12, 2026
92e79cd
fix(pool): immutable naming, deploy script, docs, invariants, arch guard
iap May 12, 2026
4291486
fix(pool): fix deploy script role grant and ASSET_LEDGER null guard
iap May 12, 2026
727364c
fix(ci): compile circuit before running witness tests
iap May 12, 2026
cf123dc
fix(ci): create build dir before circom compile
iap May 12, 2026
2c65889
refactor(pool): pre-merge improvements
iap May 13, 2026
84b1f77
chore(pool): update circuits CI, setup, and pool errors
iap May 13, 2026
4320dc5
fix(pool): address CodeRabbit review findings
iap May 13, 2026
1965863
fix(pool): address second round CodeRabbit findings
iap May 13, 2026
e56a741
feat(pool): add pool E2E test, fix RYLACreditLedger caller model
iap May 13, 2026
496d7b1
feat(pool): add ReleasePool.s.sol orchestrator and pool env vars
iap May 13, 2026
c7f6597
fix(pool): security fixes and dead code removal
iap May 13, 2026
d7ad624
fix(test): fix nullifier replay test to use fresh signatures
iap May 13, 2026
2ae41ea
fix(pool): guard totalCreditsOutstanding against underflow
iap May 13, 2026
8e9a989
feat(pool): add pool release CI check and deploy script tests
iap May 13, 2026
96faf21
fix(pool): address final CodeRabbit findings
iap May 13, 2026
8800bc2
fix(ci): fix pool release CI failure and address CodeRabbit finding
iap May 13, 2026
a1f5254
fix(ci): remove pool execute smoke, fix jq assertion, fix KI-7 wording
iap May 13, 2026
4000d97
fix(pool): add code.length checks to RYLACreditLedger constructor and…
iap May 13, 2026
8ae221f
fix(contracts): harden settlement verifier flow and CI reliability
iap May 13, 2026
bc5e875
fix(review): address open CI and pool verifier feedback
iap May 14, 2026
4c7fa7e
refactor(pool): rename min fee guard error for clarity
iap May 14, 2026
b20b999
fix(pool,settlement): replace require strings and wrong errors with c…
iap May 14, 2026
3a9249f
ci: trigger fresh CI run
iap May 14, 2026
df092a7
docs(pool): correct KI-8 — MARKPool itself is over EIP-170 size limit
iap May 14, 2026
8f36bb8
fix(pool): reduce MARKPool below EIP-170 size limit (24200 < 24576 by…
iap May 14, 2026
cc341b3
fix: address CodeRabbit findings (circuits, Makefile, architecture-gu…
iap May 14, 2026
dc43f4b
fix: address remaining CodeRabbit findings
iap May 14, 2026
94e6efc
fix(circuits): lowercase error message comparison in expectFail
iap May 14, 2026
81a70f4
docs(deployment): add Groth16SettlementVerifier wiring step (Step 18)
iap May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/circuits-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Circuits CI

on:
pull_request:
push:
branches:
- main
- canary
- dev

jobs:
circuits-test:
name: Circuits Witness Tests
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: circuits

steps:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'

- name: Install circom
run: |
CIRCOM_VERSION="v2.2.3"
CIRCOM_URL="https://github.com/iden3/circom/releases/download/${CIRCOM_VERSION}/circom-linux-amd64"
curl -L "$CIRCOM_URL" -o circom
chmod +x circom
sudo mv circom /usr/local/bin/circom
circom --version

- name: Install dependencies
run: npm ci

- name: Run witness tests
run: npm test
20 changes: 20 additions & 0 deletions .github/workflows/contracts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ jobs:
.gitCommit != null
' broadcast/mark-release-ci.json

- name: Run pool release orchestrator dry-run
run: |
TOKEN=$(jq -r '.token' broadcast/mark-release-ci.json)
POOL_VERIFIER=$(forge create src/pool/verifier/MARKPoolVerifier.sol:MARKPoolVerifier \
--rpc-url $RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--json | jq -r '.deployedTo')
if [ -z "$POOL_VERIFIER" ] || [ "$POOL_VERIFIER" = "null" ]; then
echo "failed to deploy MARKPoolVerifier for pool dry-run" >&2
exit 1
fi
MARK_RYLA_TOKEN="$TOKEN" \
MARK_POOL_VERIFIER="$POOL_VERIFIER" \
forge script script/ops/pool/ReleasePool.s.sol --rpc-url $RPC_URL -vv

# Pool execute smoke is omitted: MARKPool and PoseidonT3 exceed the EIP-170
# 24,576-byte contract size limit and cannot be broadcast to Anvil until the
# PoseidonT3 refactor (KI-8 in contracts/KNOWN_ISSUES.md) is complete.

- name: Print anvil logs on failure
if: failure()
run: tail -n 200 /tmp/anvil.log || true
Expand Down
26 changes: 26 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,32 @@ make verify-evidence-manifest
make sign-evidence-manifest
```

#### Step 18: Wire Groth16SettlementVerifier (if using ZK settlement)

After deploying `Groth16SettlementVerifier`, two post-deploy calls are required
before ZK-based settlement is active. `AttestedSettlementVerifier` remains the
fallback until this is complete.

```bash
# 1. Bind the verifier to the settlement module (prevents cross-module replay)
cast send $GROTH16_VERIFIER_ADDRESS \
"setSettlementModule(address)" $SETTLEMENT_MODULE_ADDRESS \
--rpc-url $MAINNET_RPC --private-key $DEPLOYER_KEY

# 2. Set the MARKPoolVerifier contract
cast send $GROTH16_VERIFIER_ADDRESS \
"setVerifierContract(address)" $MARK_POOL_VERIFIER_ADDRESS \
--rpc-url $MAINNET_RPC --private-key $DEPLOYER_KEY

# 3. Wire into settlement module
cast send $SETTLEMENT_MODULE_ADDRESS \
"setVerifier(address,bool)" $GROTH16_VERIFIER_ADDRESS true \
--rpc-url $MAINNET_RPC --private-key $DEPLOYER_KEY
```

See `contracts/RUNBOOK.md` → "Groth16 Direction Rollout" for the full
migration sequence before enabling production mode.

---

## Verification & Monitoring
Expand Down
4 changes: 4 additions & 0 deletions circuits/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules/
*.zkey
*.ptau
witness.wtns

# Prototype files (superseded by circuits/mark/MARKPool.circom)
utxo/
setup.js
229 changes: 229 additions & 0 deletions circuits/mark/MARKPool.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/switcher.circom";
include "circomlib/circuits/bitify.circom";

template MARKPool(depth, nIn, nOut) {
// Domain separation constants — PERMANENT. Never change after first deployment.
// These values are protocol-specific to MARK and must remain stable across upgrades
// to prevent cross-version commitment and nullifier reuse.
// DOMAIN_VERSION: 1 — protocol version tag
// DOMAIN_NOTE_COMMITMENT: 11 — note commitment hash domain
// DOMAIN_NULLIFIER: 12 — nullifier hash domain
var DOMAIN_VERSION = 1;
var DOMAIN_NOTE_COMMITMENT = 11;
var DOMAIN_NULLIFIER = 12;

// Private inputs (notes to spend)
signal input inAmount[nIn];
signal input inSecret[nIn];
signal input inBlinding[nIn];
signal input inPathElements[nIn][depth];
signal input inPathIndices[nIn][depth];

// Private outputs (new notes)
signal input outAmount[nOut];
signal input outSecret[nOut];
signal input outBlinding[nOut];

// Public inputs.
// IMPORTANT: snarkjs publicSignals ordering follows signal declaration order.
// Canonical verifier order (13 signals):
// [merkleRoot, chainId, dstChainId, protocolEpoch, fee, relayer,
// nullifier[0], nullifier[1], outCommitment[0], outCommitment[1],
// withdrawOwner, withdrawRecipient, withdrawAmount]
signal input merkleRoot;
signal input chainId;
signal input dstChainId;
signal input protocolEpoch;
signal input fee;
signal input relayer;
signal input nullifier[nIn];
signal input outCommitment[nOut];
signal input withdrawOwner;
signal input withdrawRecipient;
signal input withdrawAmount;

signal computedNullifier[nIn];
signal computedOutCommitment[nOut];

// 1) Input commitments + nullifiers
component inCommitment[nIn];
component inNullifier[nIn];
var i;
for (i = 0; i < nIn; i++) {
inCommitment[i] = Poseidon(4);
inCommitment[i].inputs[0] <== DOMAIN_VERSION * 100 + DOMAIN_NOTE_COMMITMENT;
inCommitment[i].inputs[1] <== inAmount[i];
inCommitment[i].inputs[2] <== inSecret[i];
inCommitment[i].inputs[3] <== inBlinding[i];

inNullifier[i] = Poseidon(4);
inNullifier[i].inputs[0] <== DOMAIN_VERSION * 100 + DOMAIN_NULLIFIER;
inNullifier[i].inputs[1] <== inSecret[i];
inNullifier[i].inputs[2] <== inCommitment[i].out;
inNullifier[i].inputs[3] <== chainId;
computedNullifier[i] <== inNullifier[i].out;
computedNullifier[i] === nullifier[i];
}

// Prevent zero nullifiers (double-spend protection)
component nullifierNonZero[nIn];
for (i = 0; i < nIn; i++) {
nullifierNonZero[i] = IsZero();
nullifierNonZero[i].in <== nullifier[i];
nullifierNonZero[i].out === 0;
}

// Prevent duplicate nullifiers within the same proof
component sameNullifier[nIn * (nIn - 1) / 2];
var pairIndex = 0;
for (i = 0; i < nIn; i++) {
for (var j = i + 1; j < nIn; j++) {
sameNullifier[pairIndex] = IsEqual();
sameNullifier[pairIndex].in[0] <== nullifier[i];
sameNullifier[pairIndex].in[1] <== nullifier[j];
sameNullifier[pairIndex].out === 0;
pairIndex++;
}
}

// 2) Merkle inclusion for each input
signal cur[nIn][depth + 1];
component sw[nIn][depth];
component h[nIn][depth];
var j;
for (i = 0; i < nIn; i++) {
cur[i][0] <== inCommitment[i].out;
for (j = 0; j < depth; j++) {
inPathIndices[i][j] * (inPathIndices[i][j] - 1) === 0;
sw[i][j] = Switcher();
sw[i][j].sel <== inPathIndices[i][j];
sw[i][j].L <== cur[i][j];
sw[i][j].R <== inPathElements[i][j];
h[i][j] = Poseidon(2);
h[i][j].inputs[0] <== sw[i][j].outL;
h[i][j].inputs[1] <== sw[i][j].outR;
cur[i][j + 1] <== h[i][j].out;
}
cur[i][depth] === merkleRoot;
}

// Ensure merkle root is non-zero
component merkleRootNonZero = IsZero();
merkleRootNonZero.in <== merkleRoot;
merkleRootNonZero.out === 0;

// 3) Output commitments — bound to dstChainId to prevent cross-chain replay
component outCommit[nOut];
for (i = 0; i < nOut; i++) {
outCommit[i] = Poseidon(4);
outCommit[i].inputs[0] <== DOMAIN_VERSION * 100 + DOMAIN_NOTE_COMMITMENT;
outCommit[i].inputs[1] <== outAmount[i];
outCommit[i].inputs[2] <== outSecret[i];
outCommit[i].inputs[3] <== outBlinding[i] + dstChainId;
computedOutCommitment[i] <== outCommit[i].out;
computedOutCommitment[i] === outCommitment[i];
}

// Prevent duplicate output commitments within the same proof
component sameOutCommitment[nOut * (nOut - 1) / 2];
pairIndex = 0;
for (i = 0; i < nOut; i++) {
for (j = i + 1; j < nOut; j++) {
sameOutCommitment[pairIndex] = IsEqual();
sameOutCommitment[pairIndex].in[0] <== outCommitment[i];
sameOutCommitment[pairIndex].in[1] <== outCommitment[j];
sameOutCommitment[pairIndex].out === 0;
pairIndex++;
}
}

// 4) Range constraints
component inAmountBits[nIn];
component inAmountPositive[nIn];
for (i = 0; i < nIn; i++) {
inAmountBits[i] = Num2Bits(64);
inAmountBits[i].in <== inAmount[i];

inAmountPositive[i] = GreaterThan(64);
inAmountPositive[i].in[0] <== inAmount[i];
inAmountPositive[i].in[1] <== 0;
inAmountPositive[i].out === 1;
}

component outAmountBits[nOut];
for (i = 0; i < nOut; i++) {
outAmountBits[i] = Num2Bits(64);
outAmountBits[i].in <== outAmount[i];
// Output amounts may be zero (change outputs)
}

component feeBits = Num2Bits(64);
feeBits.in <== fee;

component relayerBits = Num2Bits(160);
relayerBits.in <== relayer;

component withdrawRecipientBits = Num2Bits(160);
withdrawRecipientBits.in <== withdrawRecipient;

component withdrawOwnerBits = Num2Bits(160);
withdrawOwnerBits.in <== withdrawOwner;

component withdrawAmountBits = Num2Bits(64);
withdrawAmountBits.in <== withdrawAmount;

component dstChainBits = Num2Bits(64);
dstChainBits.in <== dstChainId;

component protocolEpochBits = Num2Bits(32);
protocolEpochBits.in <== protocolEpoch;

// 5) Balance equation: sum(inputs) = sum(outputs) + fee + withdrawAmount
// Fee rate policy is enforced at the contract level (Pool.feeBurnBps), not here.
signal sumIn[nIn + 1];
signal sumOut[nOut + 1];
sumIn[0] <== 0;
sumOut[0] <== 0;
for (i = 0; i < nIn; i++) {
sumIn[i + 1] <== sumIn[i] + inAmount[i];
}
for (i = 0; i < nOut; i++) {
sumOut[i + 1] <== sumOut[i] + outAmount[i];
}
sumIn[nIn] === sumOut[nOut] + fee + withdrawAmount;

// Withdraw binding: if withdrawAmount is zero, owner and recipient must be zero.
// If withdrawAmount is non-zero, owner and recipient must both be non-zero.
component withdrawAmountIsZero = IsZero();
withdrawAmountIsZero.in <== withdrawAmount;
withdrawOwner * withdrawAmountIsZero.out === 0;
withdrawRecipient * withdrawAmountIsZero.out === 0;

component withdrawOwnerIsZero = IsZero();
withdrawOwnerIsZero.in <== withdrawOwner;
withdrawOwnerIsZero.out * (1 - withdrawAmountIsZero.out) === 0;

component withdrawRecipientIsZero = IsZero();
withdrawRecipientIsZero.in <== withdrawRecipient;
withdrawRecipientIsZero.out * (1 - withdrawAmountIsZero.out) === 0;
}

// Public signal order (13 signals):
// [0] merkleRoot
// [1] chainId
// [2] dstChainId
// [3] protocolEpoch
// [4] fee
// [5] relayer
// [6] nullifier[0]
// [7] nullifier[1]
// [8] outCommitment[0]
// [9] outCommitment[1]
// [10] withdrawOwner
// [11] withdrawRecipient
// [12] withdrawAmount
component main {public [merkleRoot, chainId, dstChainId, protocolEpoch, fee, relayer, nullifier, outCommitment, withdrawOwner, withdrawRecipient, withdrawAmount]} = MARKPool(20, 2, 2);
4 changes: 2 additions & 2 deletions circuits/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"private": true,
"description": "MARK Protocol ZK circuits (circom)",
"scripts": {
"build": "circom utxo/UTXOSettlement.circom --r1cs --wasm --sym -l ../node_modules -l node_modules --output build",
"test": "node test/UTXOSettlement.test.mjs"
"build": "circom mark/MARKPool.circom --r1cs --wasm --sym -l node_modules --output build",
"test": "mkdir -p build && npm run build && node test/MARKPool.test.mjs"
},
"dependencies": {
"circomlib": "2.0.5"
Expand Down
Loading
Loading