Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fc56eb8
ops: enforce promotion freshness and commit lineage checks
iap Apr 26, 2026
d0044b3
ops: add full release evidence dispatch sequence
iap Apr 26, 2026
a3b2e31
ops: add release secret bootstrap helper
iap Apr 26, 2026
f77534d
ops: harden release dispatch run correlation and strict env checks
iap Apr 26, 2026
7909c58
refactor: centralize core contract custom errors
iap Apr 26, 2026
c24416a
refactor(contracts): separate bridge/settlement domains and harden se…
iap Apr 29, 2026
3b7b9f4
ci(contracts): enforce architecture/layering guards and fix refactor …
iap Apr 29, 2026
079ba13
chore(contracts): remove legacy files replaced by domain refactor
iap Apr 29, 2026
e8a7ae9
feat(ops): add canonical release-gate workflow with evidence artifact
iap Apr 29, 2026
73baf43
chore(governance): align release flow and policy guard with canary pr…
iap May 2, 2026
2b67e2d
feat(release): harden CI gates and retire cross-chain demo artifacts
iap May 2, 2026
d030dfd
chore(ci): stabilize local test and lint signal
iap May 2, 2026
b13ecbf
fix(ci): repair contracts workflow execution on GitHub
iap May 2, 2026
c5bf691
fix(ci): quote static private key in contracts-ci workflow env
iap May 2, 2026
b08e5c2
fix(slither): codify accepted detector exclusions for MARK contracts
iap May 2, 2026
202c70a
chore(ci): harden workflow runtime compatibility and add frontend nod…
iap May 2, 2026
4bc94ec
fix(frontend-ci): ensure pnpm setup works with node matrix
iap May 2, 2026
c0a98c1
fix(frontend-ci): install pnpm before setup-node auto-cache check
iap May 2, 2026
36df5ba
fix(frontend-ci): rely on packageManager-pinned pnpm version
iap May 2, 2026
623b91b
chore(ci): replace pnpm action with corepack-pinned bootstrap
iap May 2, 2026
e22bc22
fix(contracts-ci): wait for anvil before release dry-run
iap May 2, 2026
01d275e
chore(deps): add dependabot config for actions and npm
iap May 2, 2026
30722fe
chore(deps): add dependabot config for actions and npm
iap May 2, 2026
b8ef275
chore(coderabbit): add repository-level review configuration
iap May 3, 2026
b68ac41
Merge pull request #2 from iap/dev
iap May 3, 2026
da727f4
fix(readiness): run pre-checks before contracts working directory exists
iap May 3, 2026
1f318fd
chore: promote dev to canary (ci and quality sync) (#15)
iap May 3, 2026
18ca92b
ci(security): add codeql and dependency review gates
iap May 3, 2026
c0b74f6
chore: promote dev to canary
iap May 9, 2026
f9edcd8
chore: promote dev to canary
iap May 10, 2026
83bc888
chore: promote dev to canary (v0.1.1 prep)
iap May 10, 2026
3429cf5
chore: promote dev to canary
iap May 10, 2026
1eaaeb6
chore: promote dev to canary
iap May 10, 2026
88389df
chore: promote dev to canary
iap May 10, 2026
55cd585
chore: promote dev to canary
iap May 10, 2026
21dbf9b
chore: promote dev to canary
iap May 11, 2026
42acd8a
chore: promote dev to canary for OP Sepolia staging (#114)
iap May 17, 2026
0aa40dd
chore: merge main into canary to resolve divergence
iap May 17, 2026
fd9965d
ci: trigger rerun
iap May 17, 2026
d7589e1
fix: restore via_ir=true to foundry.toml (lost in merge)
iap May 17, 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:
- 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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(The MIT License)

Copyright 2020-2025 Optimism
Copyright 2026 Trade

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
9 changes: 9 additions & 0 deletions circuits/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
build/
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);
Loading
Loading