Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/e2e-sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:

- name: Install Playwright browsers
working-directory: frontend
run: npx playwright install --with-deps chromium firefox
run: npx playwright install --with-deps chromium

- name: Load .env.e2e into job env
run: |
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/zk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: ZK Circuits & Soroban Fraud Verifier

on:
push:
branches: [main, "fix/**", "feat/**"]
paths:
- "zk/**"
- "contracts/fraud_verifier/**"
pull_request:
paths:
- "zk/**"
- "contracts/fraud_verifier/**"

jobs:
soroban-verifier:
name: Soroban Fraud Verifier (cargo test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
contracts/target
key: ${{ runner.os }}-cargo-fraud-${{ hashFiles('contracts/fraud_verifier/Cargo.toml') }}
restore-keys: ${{ runner.os }}-cargo-fraud-

- name: Build WASM artifact
working-directory: contracts/fraud_verifier
run: cargo build --target wasm32-unknown-unknown --release --locked

- name: Run tests
working-directory: contracts/fraud_verifier
run: cargo test --features testutils --locked 2>&1

zk-crosscheck:
name: ZK TypeScript Cross-Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: zk/package.json

- name: Install Rust (for hash_helper)
uses: dtolnay/rust-toolchain@stable

- name: Build Poseidon hash helper for cross-check test
working-directory: contracts/fraud_verifier
run: cargo build --bin hash_helper

- name: Install dependencies (strict, no scripts)
working-directory: zk
run: npm ci --ignore-scripts

- name: Compile Circom circuits (verify they remain valid)
working-directory: zk
run: |
wget -qO circom https://github.com/iden3/circom/releases/download/v2.1.5/circom-linux-amd64
chmod +x circom
sudo mv circom /usr/local/bin/
circom circuits/fraud_proof.circom --r1cs --sym -o artifacts/
circom circuits/batch_merkle.circom --r1cs --sym -o artifacts/
circom circuits/commitment_hash.circom --r1cs --sym -o artifacts/

- name: Run cross-check tests
working-directory: zk
run: npm test
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ evm/typechain-types/
# Soroban test snapshots
test_snapshots/

# ZK build artifacts & ceremony files
zk/artifacts/
*.ptau
.env.e2e

# PR Drafts
PR_DESCRIPTION.md
78 changes: 0 additions & 78 deletions PR_DESCRIPTION.md

This file was deleted.

108 changes: 108 additions & 0 deletions backend/src/zk/fraudProofService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Address, Contract, Networks, TransactionBuilder, BASE_FEE, rpc, xdr, Keypair } from '@stellar/stellar-sdk';

declare var console: any;

export interface CommitmentOp {
issuerAddress: string;
counterpartyAddress: string;
termsHash: bigint;
dueAt: bigint;
}

export interface BatchWitness {
leafPos: bigint;
siblings: bigint[];
pathBits: (0 | 1)[];
}

export interface FraudProofInputs {
claimedBatchRoot: bigint;
operation: CommitmentOp;
issuerSignature: string; // Hex string (64 bytes)
merkleWitness: BatchWitness;
}

export class FraudProofService {
private readonly verifierContractId: string;
private readonly rpcUrl: string;

constructor(verifierContractId: string, rpcUrl: string = 'https://soroban-testnet.stellar.org') {
this.verifierContractId = verifierContractId;
this.rpcUrl = rpcUrl;
}

/**
* Called by a watcher node that detects a potentially fraudulent batch.
* Generates a ZK proof and submits it to the Soroban verifier contract.
*/
async challenge(
challengerKeypair: Keypair,
batchLedgerSeq: number,
inputs: FraudProofInputs
): Promise<{ success: boolean; txHash?: string }> {

console.log('Generating fraud proof...');
// Dynamically import the ESM module from the zk package
const zkModule = await import('../../../zk/scripts/generate_proof.js' as any);
const { generateFraudProof } = zkModule;

const { isFraud } = await generateFraudProof(inputs);

if (!isFraud) {
console.log('No fraud detected — batch state transition is correct.');
return { success: false };
}

console.log('Fraud detected! Submitting proof to Soroban...');

const server = new rpc.Server(this.rpcUrl);
const account = await server.getAccount(challengerKeypair.publicKey());
const contract = new Contract(this.verifierContractId);

// Soroban structs represented as ScMap must have keys sorted alphabetically
const commitmentScVal = xdr.ScVal.scvMap([
new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('counterparty'), val: Address.fromString(inputs.operation.counterpartyAddress).toScVal() }),
new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('due_at'), val: xdr.ScVal.scvU64(new xdr.Uint64(Number(inputs.operation.dueAt))) }),
new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('issuer'), val: Address.fromString(inputs.operation.issuerAddress).toScVal() }),
new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('terms_hash'), val: xdr.ScVal.scvBytes(Buffer.from(inputs.operation.termsHash.toString(16).padStart(64, '0'), 'hex')) }),
]);

const siblingsScVec = inputs.merkleWitness.siblings.map(sib =>
xdr.ScVal.scvBytes(Buffer.from(sib.toString(16).padStart(64, '0'), 'hex'))
);
const pathBitsScVec = inputs.merkleWitness.pathBits.map(bit =>
xdr.ScVal.scvU32(bit)
);

const merkleProofScVal = xdr.ScVal.scvMap([
new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('path_bits'), val: xdr.ScVal.scvVec(pathBitsScVec) }),
new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol('siblings'), val: xdr.ScVal.scvVec(siblingsScVec) }),
]);

const tx = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: Networks.TESTNET,
})
.addOperation(
contract.call(
'submit_fraud_proof',
Address.fromString(challengerKeypair.publicKey()).toScVal(),
xdr.ScVal.scvU32(batchLedgerSeq),
xdr.ScVal.scvU32(Number(inputs.merkleWitness.leafPos)),
commitmentScVal,
merkleProofScVal
)
)
.setTimeout(30)
.build();

const simResult = await server.simulateTransaction(tx);
console.log(`Estimated CPU instructions: ${(simResult as any).cost?.cpuInsns}`);

const preparedTx = await server.prepareTransaction(tx);
preparedTx.sign(challengerKeypair);
const response = await server.sendTransaction(preparedTx);

return { success: true, txHash: response.hash };
}
}
Loading
Loading