Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 11 additions & 6 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ REDIS_URL=redis://localhost:6379
# ─── BNB Chain RPC ────────────────────────────────────────────────────────────
BNB_RPC_URL=https://bsc-dataseed.binance.org/
BNB_TESTNET_RPC_URL=https://data-seed-prebsc-1-s1.binance.org:8545/
CHAIN_ID=56
CHAIN_ID=97

# ─── Relayer Wallet ───────────────────────────────────────────────────────────
# HOT wallet used only for submitting transactions — keep minimal BNB balance
RELAYER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
RELAYER_ADDRESS=0xYOUR_RELAYER_ADDRESS_HERE

# ─── Smart Contracts ──────────────────────────────────────────────────────────
PAYMASTER_CONTRACT_ADDRESS=0xYOUR_PAYMASTER_CONTRACT_ADDRESS
GHOST_POOL_ADDRESS=0xYOUR_GHOST_POOL_ADDRESS
ENTRYPOINT_ADDRESS=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2781
# ─── Smart Contracts (BNB Testnet — chainId 97) ───────────────────────────────
# Deployed 2026-02-27 — see contracts/deployments/97/addresses.json
PAYMASTER_CONTRACT_ADDRESS=0x635f53515113C27f0ec0dE30aD030184487508b0
GHOST_POOL_ADDRESS=0x154Fcb02A72E65a5c9Bc155E75CCFf16D0825bee
ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032

# ─── Bundler (Pimlico / Alchemy) ──────────────────────────────────────────────
BUNDLER_URL=https://api.pimlico.io/v2/56/rpc
BUNDLER_URL=https://api.pimlico.io/v2/97/rpc
PIMLICO_API_KEY=pim_YOUR_API_KEY_HERE

# ─── ZK Circuit Artifacts ─────────────────────────────────────────────────────
Expand All @@ -45,3 +46,7 @@ ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001

# ─── Logging ──────────────────────────────────────────────────────────────────
LOG_LEVEL=info

# ─── Indexer ─────────────────────────────────────────────────────────────────
# How often the deposit event indexer polls for new on-chain Deposit events (ms)
INDEXER_POLL_INTERVAL_MS=15000
14 changes: 7 additions & 7 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion backend/src/api/routes/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { z } from 'zod';
import { depositRepo } from '../../db/repositories/depositRepo.js';
import { merkleTree } from '../../zk/merkleTree.js';
import { logger } from '../../utils/logger.js';
import { isCommitmentInserted, getOnChainRoot, getNextLeafIndex } from '../../relayer/paymasterClient.js';
import type { Hex } from 'viem';

const DepositSchema = z.object({
commitment: z.string().startsWith('0x').length(66),
Expand Down Expand Up @@ -41,6 +43,16 @@ const poolRoutes: FastifyPluginAsync = async (fastify) => {
});
}

// Verify the commitment exists on-chain before accepting it
const onChain = await isCommitmentInserted(commitment as Hex).catch(() => false);
if (!onChain) {
return reply.status(400).send({
error: 'COMMITMENT_NOT_ON_CHAIN',
message: 'This commitment has not been inserted into the on-chain GhostPool yet.',
details: { commitment },
});
}

// Insert into Merkle tree
const commitment256 = BigInt(commitment);
const { root: newRoot, leafIndex } = await merkleTree.insert(commitment256);
Expand All @@ -67,10 +79,12 @@ const poolRoutes: FastifyPluginAsync = async (fastify) => {

// GET /v1/pool/status
fastify.get('/pool/status', async (_request, reply) => {
const [totalDeposits, nextLeafIndex, root] = await Promise.all([
const [totalDeposits, nextLeafIndex, root, onChainRoot, onChainLeafIndex] = await Promise.all([
depositRepo.countTotal(),
Promise.resolve(merkleTree.getNextIndex()),
Promise.resolve(merkleTree.getCurrentRoot()),
getOnChainRoot().catch(() => null),
getNextLeafIndex().catch(() => null),
]);

const merkleRootHex = '0x' + root.toString(16).padStart(64, '0');
Expand All @@ -81,6 +95,10 @@ const poolRoutes: FastifyPluginAsync = async (fastify) => {
merkleRoot: merkleRootHex,
merkleTreeHeight: Number(process.env['MERKLE_TREE_HEIGHT'] ?? 20),
anonymitySetSize: nextLeafIndex,
onChain: {
merkleRoot: onChainRoot ?? null,
nextLeafIndex: onChainLeafIndex ?? null,
},
});
});

Expand Down
14 changes: 12 additions & 2 deletions backend/src/api/routes/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { nullifierRepo } from '../../db/repositories/nullifierRepo.js';
import { operationRepo } from '../../db/repositories/operationRepo.js';
import { enqueueRelayJob } from '../../relayer/queue.js';
import { aspCheckBatch } from '../../compliance/asp.js';
import { isKnownRoot, checkContractHealth } from '../../relayer/paymasterClient.js';
import { isKnownRoot, isNullifierSpentOnChain } from '../../relayer/paymasterClient.js';
import { validatePaymasterAddress, validateChainId } from '../../relayer/userOpBuilder.js';
import { logger } from '../../utils/logger.js';
import type { Hex } from 'viem';
Expand Down Expand Up @@ -73,7 +73,7 @@ const relayRoutes: FastifyPluginAsync = async (fastify) => {

const { merkleRoot, nullifierHash } = signals;

// ── Nullifier double-spend check ──────────────────────────────────────
// ── Nullifier double-spend check (local DB) ───────────────────────────
const alreadySpent = await nullifierRepo.isSpent(nullifierHash);
if (alreadySpent) {
return reply.status(400).send({
Expand All @@ -83,6 +83,16 @@ const relayRoutes: FastifyPluginAsync = async (fastify) => {
});
}

// ── Nullifier double-spend check (on-chain) ───────────────────────────
const spentOnChain = await isNullifierSpentOnChain(nullifierHash as Hex).catch(() => false);
if (spentOnChain) {
return reply.status(400).send({
error: 'PROOF_ALREADY_SPENT_ON_CHAIN',
message: 'This nullifier has already been spent on-chain.',
details: { nullifierHash },
});
}

// ── On-chain Merkle root check ─────────────────────────────────────────
const rootKnown = await isKnownRoot(merkleRoot as Hex).catch(() => false);
if (!rootKnown) {
Expand Down
106 changes: 106 additions & 0 deletions backend/src/contracts/abis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Minimal ABIs for Ghost Privacy Suite contracts.
* Only includes the functions and events the backend needs to call.
*/

export const GHOST_POOL_ABI = [
// ─── View functions ────────────────────────────────────────────────────
{
name: 'getLastRoot',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'bytes32' }],
},
{
name: 'isKnownRoot',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'root', type: 'bytes32' }],
outputs: [{ name: '', type: 'bool' }],
},
{
name: 'isCommitmentInserted',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'commitment', type: 'bytes32' }],
outputs: [{ name: '', type: 'bool' }],
},
{
name: 'nextLeafIndex',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'uint32' }],
},
{
name: 'poolBalance',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'token', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
},
// ─── Events ────────────────────────────────────────────────────────────
{
name: 'Deposit',
type: 'event',
inputs: [
{ name: 'commitment', type: 'bytes32', indexed: true },
{ name: 'leafIndex', type: 'uint32', indexed: true },
{ name: 'amount', type: 'uint256', indexed: false },
{ name: 'token', type: 'address', indexed: true },
{ name: 'timestamp', type: 'uint256', indexed: false },
],
},
{
name: 'FeeDeducted',
type: 'event',
inputs: [
{ name: 'nullifierHash', type: 'bytes32', indexed: true },
{ name: 'amount', type: 'uint256', indexed: false },
{ name: 'token', type: 'address', indexed: false },
],
},
] as const;

export const GHOST_PAYMASTER_ABI = [
// ─── View functions ────────────────────────────────────────────────────
{
name: 'nullifiers',
type: 'function',
stateMutability: 'view',
inputs: [{ name: '', type: 'bytes32' }],
outputs: [{ name: '', type: 'bool' }],
},
{
name: 'bnbToUsdcRate',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'uint256' }],
},
{
name: 'zkVerificationEnabled',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'bool' }],
},
// ─── Events ────────────────────────────────────────────────────────────
{
name: 'NullifierSpent',
type: 'event',
inputs: [{ name: 'nullifierHash', type: 'bytes32', indexed: true }],
},
{
name: 'GasSponsored',
type: 'event',
inputs: [
{ name: 'sender', type: 'address', indexed: true },
{ name: 'nullifierHash', type: 'bytes32', indexed: true },
{ name: 'gasCostWei', type: 'uint256', indexed: false },
{ name: 'feeUSDC', type: 'uint256', indexed: false },
{ name: 'feeToken', type: 'address', indexed: false },
],
},
] as const;
59 changes: 59 additions & 0 deletions backend/src/contracts/addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Contract address registry.
*
* Returns the deployed contract addresses for a given chainId.
* Falls back to environment-variable overrides so you can point at
* a local Hardhat node without touching the JSON file.
*/

import { config } from '../config.js';

export interface ContractAddresses {
ghostPool: `0x${string}`;
ghostPaymaster: `0x${string}`;
entryPoint: `0x${string}`;
usdc: `0x${string}`;
usdt: `0x${string}`;
}

// ─── BNB Testnet (chainId 97) ─────────────────────────────────────────────────

const TESTNET_ADDRESSES: ContractAddresses = {
ghostPool: '0x154Fcb02A72E65a5c9Bc155E75CCFf16D0825bee',
ghostPaymaster: '0x635f53515113C27f0ec0dE30aD030184487508b0',
entryPoint: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
usdc: '0x6338e666BfA41e1fE638e8eF57CbCcA60D452872',
usdt: '0xab6a28bDEAB06d0902F5465097821F5A5BCd896d',
};

// ─── BNB Mainnet (chainId 56) ─────────────────────────────────────────────────

const MAINNET_ADDRESSES: ContractAddresses = {
// Populated when contracts are deployed to mainnet
ghostPool: '0x0000000000000000000000000000000000000000',
ghostPaymaster: '0x0000000000000000000000000000000000000000',
entryPoint: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
usdc: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', // BSC USDC
usdt: '0x55d398326f99059fF775485246999027B3197955', // BSC-Peg USDT
};

const DEPLOYMENT_MAP: Record<number, ContractAddresses> = {
97: TESTNET_ADDRESSES,
56: MAINNET_ADDRESSES,
};

/**
* Get contract addresses for the configured chain.
* Environment variables always take precedence so local/testnet overrides work.
*/
export function getContractAddresses(): ContractAddresses {
const base = DEPLOYMENT_MAP[config.CHAIN_ID] ?? TESTNET_ADDRESSES;

return {
ghostPool: (config.GHOST_POOL_ADDRESS as `0x${string}`) || base.ghostPool,
ghostPaymaster: (config.PAYMASTER_CONTRACT_ADDRESS as `0x${string}`) || base.ghostPaymaster,
entryPoint: (config.ENTRYPOINT_ADDRESS as `0x${string}`) || base.entryPoint,
usdc: base.usdc,
usdt: base.usdt,
};
}
5 changes: 5 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { connectDatabase, disconnectDatabase } from './db/prisma/client.js';
import { merkleTree } from './zk/merkleTree.js';
import { startOfacSyncJob, stopOfacSyncJob } from './compliance/ofac.js';
import { createRelayWorker, redis } from './relayer/queue.js';
import { startDepositIndexer, stopDepositIndexer } from './indexer/depositIndexer.js';
import { checkBundlerHealth } from './relayer/bundlerClient.js';
import { checkContractHealth } from './relayer/paymasterClient.js';
import { checkQueueHealth } from './relayer/queue.js';
Expand Down Expand Up @@ -93,6 +94,9 @@ async function main() {
// Start OFAC sync job
startOfacSyncJob();

// Start on-chain deposit event indexer
startDepositIndexer();

// Start BullMQ worker (in-process for simplicity; can be separated)
const worker = createRelayWorker();

Expand All @@ -113,6 +117,7 @@ async function main() {
await server.close();
await worker.close();
stopOfacSyncJob();
stopDepositIndexer();
await disconnectDatabase();
await redis.quit();
logger.info('Shutdown complete');
Expand Down
Loading