diff --git a/devnet/0-all.sh b/devnet/0-all.sh index 24d710b..ca41a17 100755 --- a/devnet/0-all.sh +++ b/devnet/0-all.sh @@ -6,4 +6,5 @@ set -e ./3-op-init.sh ./4-op-start-service.sh ./5-run-op-succinct.sh -./6-run-kailua.sh \ No newline at end of file +./6-run-kailua.sh +./7-run-railgain.sh \ No newline at end of file diff --git a/devnet/7-run-railgain.sh b/devnet/7-run-railgain.sh new file mode 100755 index 0000000..49025b0 --- /dev/null +++ b/devnet/7-run-railgain.sh @@ -0,0 +1,140 @@ +#!/bin/bash +set -e + +PWD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RAILGUN_DIR="$PWD_DIR/railgun" +RAILGUN_ENV_FILE="$RAILGUN_DIR/.env.railgun" + +# Load environment variables +source .env + +# Load RAILGUN internal configuration (if exists) +if [ -f "$RAILGUN_ENV_FILE" ]; then + source "$RAILGUN_ENV_FILE" +fi + +sed_inplace() { + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +if [ "$RAILGUN_ENABLE" != "true" ]; then + echo "โญ๏ธ Skipping RAILGUN (RAILGUN_ENABLE=$RAILGUN_ENABLE)" + exit 0 +fi + +if [ ! -f "$RAILGUN_ENV_FILE" ]; then + cp "$RAILGUN_DIR/example.env.railgun" "$RAILGUN_ENV_FILE" +fi + +echo "๐Ÿ“œ Step 1/3: Deploying RAILGUN Contracts (Docker)" + +RAILGUN_CONTRACT_IMAGE_TAG="${RAILGUN_CONTRACT_IMAGE_TAG:-railgun-contract:latest}" + +# Deploy contracts using Docker +echo "๐Ÿ“œ Deploying RAILGUN smart contracts using Docker..." +echo " โ„น๏ธ Network: xlayer-devnet" +echo " โ„น๏ธ RPC: $L2_RPC_URL" +echo " โ„น๏ธ Chain ID: $CHAIN_ID" + +TEMP_DEPLOY_LOG="/tmp/railgun-deploy-$$.log" + +docker run --rm \ + -e RPC_URL="${L2_RPC_URL/localhost/host.docker.internal}" \ + -e CHAIN_ID="$CHAIN_ID" \ + -e DEPLOYER_PRIVATE_KEY="$OP_PROPOSER_PRIVATE_KEY" \ + --add-host=host.docker.internal:host-gateway \ + "$RAILGUN_CONTRACT_IMAGE_TAG" \ + deploy:test --network xlayer-devnet 2>&1 | tee "$TEMP_DEPLOY_LOG" + +DEPLOY_STATUS=${PIPESTATUS[0]} +DEPLOY_OUTPUT=$(cat "$TEMP_DEPLOY_LOG") + +if [ $DEPLOY_STATUS -ne 0 ]; then + echo " โŒ Contract deployment failed" + rm -f "$TEMP_DEPLOY_LOG" 2>/dev/null + exit 1 +fi +echo " โœ… Contracts deployed successfully" + +echo "๐Ÿ” Extracting contract addresses..." +PROXY_ADDR=$(echo "$DEPLOY_OUTPUT" | grep -A 20 "DEPLOY CONFIG:" | grep "proxy:" | sed -n "s/.*proxy: '\([^']*\)'.*/\1/p" | head -1) +RELAY_ADAPT_ADDR=$(echo "$DEPLOY_OUTPUT" | grep -A 20 "DEPLOY CONFIG:" | grep "relayAdapt:" | sed -n "s/.*relayAdapt: '\([^']*\)'.*/\1/p" | head -1) +POSEIDON_T4_ADDR=$(echo "$DEPLOY_OUTPUT" | grep -A 20 "DEPLOY CONFIG:" | grep "poseidonT4:" | sed -n "s/.*poseidonT4: '\([^']*\)'.*/\1/p" | head -1) + +if [ -n "$PROXY_ADDR" ] && [ "$PROXY_ADDR" != "null" ]; then + echo " โœ… Found RailgunSmartWallet (proxy): $PROXY_ADDR" + export RAILGUN_SMART_WALLET_ADDRESS="$PROXY_ADDR" + sed_inplace "s|^RAILGUN_SMART_WALLET_ADDRESS=.*|RAILGUN_SMART_WALLET_ADDRESS=$RAILGUN_SMART_WALLET_ADDRESS|" "$RAILGUN_ENV_FILE" +fi + +if [ -n "$RELAY_ADAPT_ADDR" ] && [ "$RELAY_ADAPT_ADDR" != "null" ]; then + echo " โœ… Found RelayAdapt: $RELAY_ADAPT_ADDR" + export RAILGUN_RELAY_ADAPT_ADDRESS="$RELAY_ADAPT_ADDR" + sed_inplace "s|^RAILGUN_RELAY_ADAPT_ADDRESS=.*|RAILGUN_RELAY_ADAPT_ADDRESS=$RELAY_ADAPT_ADDR|" "$RAILGUN_ENV_FILE" +fi + +if [ -n "$POSEIDON_T4_ADDR" ] && [ "$POSEIDON_T4_ADDR" != "null" ]; then + echo " โœ… Found PoseidonT4: $POSEIDON_T4_ADDR" + export RAILGUN_POSEIDONT4_ADDRESS="$POSEIDON_T4_ADDR" + sed_inplace "s|^RAILGUN_POSEIDONT4_ADDRESS=.*|RAILGUN_POSEIDONT4_ADDRESS=$POSEIDON_T4_ADDR|" "$RAILGUN_ENV_FILE" +fi + +echo "๐Ÿ” Verifying contract deployment..." + +VERIFICATION_RESPONSE=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getCode\",\"params\":[\"$RAILGUN_SMART_WALLET_ADDRESS\",\"latest\"],\"id\":1}" \ + "$L2_RPC_URL" 2>/dev/null) + +if echo "$VERIFICATION_RESPONSE" | grep -q '"result":"0x"'; then + echo " โŒ Contract not found at address: $RAILGUN_SMART_WALLET_ADDRESS" + exit 1 +else + echo " โœ… Contract verified on L2" +fi + +echo "๐Ÿ” Getting deployment block height..." + +BLOCK_RESPONSE=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "$L2_RPC_URL" 2>/dev/null) + +DEPLOY_BLOCK=$(echo "$BLOCK_RESPONSE" | grep -o '"result":"0x[^"]*"' | cut -d'"' -f4) +DEPLOY_BLOCK_DEC=$((16#${DEPLOY_BLOCK#0x})) + +if [ -n "$DEPLOY_BLOCK_DEC" ]; then + echo " โœ… Deployment block: $DEPLOY_BLOCK_DEC" + export RAILGUN_DEPLOY_BLOCK="$DEPLOY_BLOCK_DEC" + + # Save to railgun/.env.railgun + if grep -q "^RAILGUN_DEPLOY_BLOCK=" "$RAILGUN_ENV_FILE"; then + sed_inplace "s|^RAILGUN_DEPLOY_BLOCK=.*|RAILGUN_DEPLOY_BLOCK=$RAILGUN_DEPLOY_BLOCK|" "$RAILGUN_ENV_FILE" + else + echo "RAILGUN_DEPLOY_BLOCK=$RAILGUN_DEPLOY_BLOCK" >> "$RAILGUN_ENV_FILE" + fi +else + echo " โš ๏ธ Could not determine deployment block, using 0" + export RAILGUN_DEPLOY_BLOCK="0" +fi + +rm -f "$TEMP_DEPLOY_LOG" 2>/dev/null +echo "โœ… Contract deployment completed" + +echo "๐Ÿช™ Step 2/3: Deploying Test ERC20 Token" +./scripts/deploy-test-token.sh || { + echo "โŒ Failed to deploy test token" + exit 1 +} + +echo "๐Ÿงช Step 3/3: Run Wallet Tests" +./scripts/run-railgun-wallet.sh || { + echo "โŒ Wallet test failed" + exit 1 +} + +echo "๐ŸŽ‰ Complete Railgun Flow Finished Successfully!" \ No newline at end of file diff --git a/devnet/example.env b/devnet/example.env index ac35584..6ee94d8 100644 --- a/devnet/example.env +++ b/devnet/example.env @@ -55,6 +55,19 @@ KAILUA_LOCAL_DIRECTORY= SKIP_KAILUA_BUILD=true KAILUA_IMAGE_TAG=kailua:latest +# ============================================================================== +# RAILGUN Privacy System Configuration +# ============================================================================== +RAILGUN_ENABLE=false +# from: https://github.com/Railgun-Privacy/contract.git +RAILGUN_CONTRACT_DIR= +RAILGUN_CONTRACT_IMAGE_TAG=railgun-contract:latest +SKIP_RAILGUN_CONTRACT_BUILD=true +# from: https://github.com/ethereum/kohaku.git +RAILGUN_KOHAKU_LOCAL_DIRECTORY= +RAILGUN_SDK_IMAGE_TAG=railgun-sdk:latest +SKIP_RAILGUN_SDK_BUILD=true + # ============================================================================== # Build Configuration # ============================================================================== diff --git a/devnet/init.sh b/devnet/init.sh index 64afbf2..6a0b057 100755 --- a/devnet/init.sh +++ b/devnet/init.sh @@ -152,3 +152,38 @@ else build_and_tag_image "kailua" "$KAILUA_IMAGE_TAG" "$KAILUA_LOCAL_DIRECTORY" "Dockerfile.local" fi fi + +# Build RAILGUN SDK Test image +if [ "$SKIP_RAILGUN_SDK_BUILD" = "true" ]; then + echo "โญ๏ธ Skipping railgun SDK build" +else + if [ -z "$RAILGUN_KOHAKU_LOCAL_DIRECTORY" ]; then + echo "โŒ Please set RAILGUN_KOHAKU_LOCAL_DIRECTORY in .env" + exit 1 + fi + echo "๐Ÿ”จ Building railgun SDK image" + docker build -t "${RAILGUN_SDK_IMAGE_TAG:-railgun-sdk:latest}" \ + -f "$PWD_DIR/railgun/Dockerfile.sdk" \ + --build-context kohaku="$RAILGUN_KOHAKU_LOCAL_DIRECTORY" \ + --progress=plain \ + "$PWD_DIR/railgun" + echo "โœ… Built: ${RAILGUN_SDK_IMAGE_TAG:-railgun-sdk:latest}" +fi + +# Build RAILGUN Contract Deploy image +if [ "$SKIP_RAILGUN_CONTRACT_BUILD" = "true" ]; then + echo "โญ๏ธ Skipping railgun contract build" +else + if [ -z "$RAILGUN_CONTRACT_DIR" ]; then + echo "โŒ Please set RAILGUN_CONTRACT_DIR in .env" + exit 1 + fi + + echo "๐Ÿ”จ Building railgun contract image" + docker build -t "${RAILGUN_CONTRACT_IMAGE_TAG:-railgun-contract:latest}" \ + -f "$PWD_DIR/railgun/Dockerfile.contract" \ + --build-context contract="$RAILGUN_CONTRACT_DIR" \ + --progress=plain \ + "$PWD_DIR/railgun" + echo "โœ… Built: ${RAILGUN_CONTRACT_IMAGE_TAG:-railgun-contract:latest}" +fi \ No newline at end of file diff --git a/devnet/railgun/Dockerfile.contract b/devnet/railgun/Dockerfile.contract new file mode 100644 index 0000000..43e59a8 --- /dev/null +++ b/devnet/railgun/Dockerfile.contract @@ -0,0 +1,75 @@ +FROM node:18-alpine AS builder + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache python3 make g++ + +# Copy contract source from build context +COPY --from=contract . /build/contract + +WORKDIR /build/contract + +# Modification 1: Add xlayer-devnet network to hardhat.config.ts +RUN sed -i '/^};$/i\ + networks: {\ + "xlayer-devnet": {\ + url: process.env.RPC_URL || "http://localhost:8123",\ + chainId: parseInt(process.env.CHAIN_ID || "195"),\ + accounts: process.env.DEPLOYER_PRIVATE_KEY ? [process.env.DEPLOYER_PRIVATE_KEY] : [],\ + gasPrice: 1000000000,\ + },\ + },' hardhat.config.ts + +# Modification 2: Add logVerify for PoseidonT3 +RUN sed -i '/const poseidonT3 = await PoseidonT3\.deploy();/a\ + await logVerify('\''PoseidonT3'\'', poseidonT3, []);' \ + tasks/deploy/test.ts + +# Modification 3: Add logVerify for PoseidonT4 +RUN sed -i '/const poseidonT4 = await PoseidonT4\.deploy();/a\ + await logVerify('\''PoseidonT4'\'', poseidonT4, []);' \ + tasks/deploy/test.ts + +# Modification 4: Add poseidon addresses to deployment output +RUN sed -i '/implementation: implementation\.address,/a\ + poseidonT3: poseidonT3.address,\ + poseidonT4: poseidonT4.address,' \ + tasks/deploy/test.ts + +# Verify modifications were applied +RUN echo "โœ… Checking modifications..." && \ + grep -q "xlayer-devnet" hardhat.config.ts && \ + echo " โœ“ hardhat.config.ts modified" && \ + grep -q "logVerify.*PoseidonT3" tasks/deploy/test.ts && \ + echo " โœ“ PoseidonT3 logVerify added" && \ + grep -q "logVerify.*PoseidonT4" tasks/deploy/test.ts && \ + echo " โœ“ PoseidonT4 logVerify added" && \ + grep -q "poseidonT3: poseidonT3.address" tasks/deploy/test.ts && \ + echo " โœ“ Poseidon addresses added to output" || \ + (echo "โŒ Modifications failed" && exit 1) + +RUN if [ -f yarn.lock ]; then \ + echo "๐Ÿ“ฆ Installing with yarn..." && \ + yarn install --frozen-lockfile --network-timeout 300000; \ + else \ + echo "๐Ÿ“ฆ Installing with npm..." && \ + npm install; \ + fi + +FROM node:18-alpine + +WORKDIR /app + +# Copy built contract with modifications +COPY --from=builder /build/contract /app + +# Set default environment variables +ENV NETWORK=xlayer-devnet +ENV RPC_URL=http://host.docker.internal:8123 +ENV CHAIN_ID=195 + +# Entrypoint: run hardhat deploy +ENTRYPOINT ["npx", "hardhat"] +CMD ["deploy:test", "--network", "xlayer-devnet"] + diff --git a/devnet/railgun/Dockerfile.sdk b/devnet/railgun/Dockerfile.sdk new file mode 100644 index 0000000..b503e6c --- /dev/null +++ b/devnet/railgun/Dockerfile.sdk @@ -0,0 +1,69 @@ +# Stage 1: Build Kohaku from external source +FROM node:22-alpine AS kohaku-builder + +WORKDIR /build + +# Install build dependencies +RUN apk add --no-cache python3 make g++ + +# Install pnpm +RUN npm install -g pnpm + +# Copy Kohaku source (from build context) +COPY --from=kohaku . /build/kohaku + +WORKDIR /build/kohaku + +# Modify circuit-artifacts URL in railgun package +RUN cd packages/railgun && \ + sed -i 's|https://npm.railgun.org/railgun-community-circuit-artifacts-0.0.1.tgz|https://ipfs-lb.com/ipfs/QmPvs6Lws1MS4CTMAQ6P3WfyFpWFKGu7o6cSbWCu3vgXLW/railgun-circuit-test-artifacts-0.0.1.tgz|g' package.json + +# Install and build with network retries +RUN pnpm install --no-frozen-lockfile \ + --fetch-retries=5 \ + --fetch-retry-mintimeout=20000 \ + --fetch-retry-maxtimeout=120000 && \ + pnpm -r build || true + +# Verify railgun package was built +RUN test -f packages/railgun/dist/index.d.ts || (echo "โŒ Railgun build failed" && exit 1) + +# Pack the railgun package +RUN cd packages/railgun && \ + pnpm pack && \ + mv *.tgz /build/kohaku-railgun.tgz + +# ============================================================================ +# Stage 2: Runtime image +# ============================================================================ +FROM node:22-alpine + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy built Kohaku package from builder +COPY --from=kohaku-builder /build/kohaku-railgun.tgz ./kohaku-railgun.tgz + +# Copy application files +COPY package.json ./ +COPY test-kohaku.ts ./ + +# Install test dependencies (using local kohaku package) +# Note: Need devDependencies (tsx, typescript) to run tests +RUN pnpm install --no-frozen-lockfile + +# Cleanup +RUN rm -f kohaku-railgun.tgz + +# Set default environment variables +ENV CHAIN_ID=195 +ENV RPC_URL=http://host.docker.internal:8123 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "console.log('OK')" || exit 1 + +ENTRYPOINT ["pnpm", "test:kohaku"] + diff --git a/devnet/railgun/example.env.railgun b/devnet/railgun/example.env.railgun new file mode 100644 index 0000000..7410997 --- /dev/null +++ b/devnet/railgun/example.env.railgun @@ -0,0 +1,6 @@ +# auto-generated +RAILGUN_SMART_WALLET_ADDRESS= +RAILGUN_RELAY_ADAPT_ADDRESS= +RAILGUN_POSEIDONT4_ADDRESS= +RAILGUN_TEST_TOKEN_ADDRESS= +RAILGUN_DEPLOY_BLOCK= diff --git a/devnet/railgun/package.json b/devnet/railgun/package.json new file mode 100644 index 0000000..b470a7f --- /dev/null +++ b/devnet/railgun/package.json @@ -0,0 +1,24 @@ +{ + "name": "railgun-devnet-test", + "version": "1.0.0", + "description": "RAILGUN DevNet Integration Test (Kohaku SDK)", + "main": "test-kohaku.ts", + "scripts": { + "test": "tsx test-kohaku.ts", + "test:kohaku": "tsx test-kohaku.ts" + }, + "dependencies": { + "@kohaku-eth/railgun": "file:./kohaku-railgun.tgz", + "ethers": "^6.10.0", + "snarkjs": "^0.7.5", + "@railgun-community/circuit-artifacts": "https://ipfs-lb.com/ipfs/QmPvs6Lws1MS4CTMAQ6P3WfyFpWFKGu7o6cSbWCu3vgXLW/railgun-circuit-test-artifacts-0.0.1.tgz" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/devnet/railgun/test-kohaku.ts b/devnet/railgun/test-kohaku.ts new file mode 100644 index 0000000..305a892 --- /dev/null +++ b/devnet/railgun/test-kohaku.ts @@ -0,0 +1,460 @@ +import { ethers } from 'ethers'; +import { + createRailgunAccount, + createRailgunIndexer, + EthersProviderAdapter, + EthersSignerAdapter, + type RailgunNetworkConfig, +} from '@kohaku-eth/railgun'; + +const CONFIG = { + chainId: parseInt(process.env.CHAIN_ID || '195'), + chainName: process.env.CHAIN_NAME || 'XLayerDevNet', + rpcUrl: process.env.RPC_URL || 'http://localhost:8123', + railgunAddress: process.env.RAILGUN_ADDRESS || '', + relayAdaptAddress: process.env.RAILGUN_RELAY_ADAPT_ADDRESS || '', + poseidonAddress: process.env.POSEIDON_ADDRESS || '', + + // Account A (Alice) - deployer, has tokens + accountA: { + privateKey: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', + }, + + // Account B (Bob) - receiver + accountB: { + privateKey: '0x169b6b7ae0857ff7ad563e6db5b7d0d0f5c3f388bc734e05b63ad05600bde341', + address: '0x430959e66fd9f6da6F96e10E04004c7e9E4A59D0', + }, + + testAmount: ethers.parseEther('500'), // 500 tokens for Shield + transferAmount: ethers.parseEther('100'), // 100 tokens for Transfer + gasFee: ethers.parseEther('1'), // 1 ETH for gas +}; + +// ERC20 ABI +const TOKEN_ARTIFACT = { + abi: [ + 'function approve(address spender, uint256 amount) returns (bool)', + 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint8)', + 'function symbol() view returns (string)', + 'function transfer(address to, uint256 amount) returns (bool)', + ] +}; + +// Global variables +let provider: ethers.JsonRpcProvider; +let signerA: ethers.Wallet; +let signerB: ethers.Wallet; +let tokenContract: ethers.Contract; +let tokenAddress: string; + +// Kohaku objects +let devnetConfig: RailgunNetworkConfig; +let indexer: Awaited>; +let aliceAccount: Awaited>; +let bobAccount: Awaited>; + +async function setupEnvironment() { + console.log('๐Ÿ“‹ Step 1: Environment Setup'); + + provider = new ethers.JsonRpcProvider(CONFIG.rpcUrl); + signerA = new ethers.Wallet(CONFIG.accountA.privateKey, provider); + signerB = new ethers.Wallet(CONFIG.accountB.privateKey, provider); + + console.log('๐Ÿ“‹ Configuration:'); + console.log(` Alice (A): ${signerA.address}`); + console.log(` Bob (B): ${signerB.address}`); + console.log(` RAILGUN: ${CONFIG.railgunAddress}\n`); + + // 1. Send gas fee to Bob + console.log('๐Ÿ“ค Sending gas fee to Bob...'); + const tx = await signerA.sendTransaction({ + to: signerB.address, + value: CONFIG.gasFee, + }); + await tx.wait(); + console.log(` โœ“ Sent ${ethers.formatEther(CONFIG.gasFee)} ETH to Bob\n`); + + // 2. Get ERC20 token from environment + console.log('๐Ÿ“ฆ Loading ERC20 token...'); + + tokenAddress = process.env.TOKEN_ADDRESS || ''; + if (!tokenAddress) { + throw new Error('TOKEN_ADDRESS not set. Please run deploy-test-token.sh first.'); + } + + tokenContract = new ethers.Contract(tokenAddress, TOKEN_ARTIFACT.abi, signerA); + + try { + const code = await provider.getCode(tokenAddress); + if (code === '0x' || code === '0x0') { + throw new Error(`Token contract not found at ${tokenAddress}`); + } + + const symbol = await tokenContract.symbol(); + const balanceA = await tokenContract.balanceOf(signerA.address); + console.log(` โœ“ Token loaded: ${tokenAddress}`); + console.log(` โœ“ Symbol: ${symbol}`); + console.log(` โœ“ Alice balance: ${ethers.formatEther(balanceA)} ${symbol}\n`); + } catch (error: any) { + console.error(` โŒ Failed to load token: ${error.message}`); + throw new Error(`Token contract at ${tokenAddress} is invalid or not deployed`); + } +} + +async function setupKohakuRailgun() { + console.log('๐Ÿ”ง Step 2: Setup Kohaku RAILGUN SDK'); + + // 1. Create custom devnet network configuration + console.log(' ๐Ÿ“ Creating devnet network configuration...'); + + // Get deployment block from environment or use 0 + let startBlock = 0; + if (process.env.RAILGUN_DEPLOY_BLOCK) { + startBlock = parseInt(process.env.RAILGUN_DEPLOY_BLOCK); + console.log(` โœ“ Using deployment block: ${startBlock}\n`); + } else { + // Fallback: estimate from current block + try { + const currentBlock = await provider.getBlockNumber(); + startBlock = Math.max(0, currentBlock - 1000); + console.log(` โš ๏ธ RAILGUN_DEPLOY_BLOCK not set, estimating: ${startBlock} (current: ${currentBlock})\n`); + } catch (error: any) { + console.log(` โš ๏ธ Could not determine start block, using 0\n`); + } + } + + // Note: For devnet, we use a wrapped ETH address placeholder + // You may need to deploy a WETH contract for native ETH shielding + const WETH_PLACEHOLDER = '0x0000000000000000000000000000000000000001'; + + devnetConfig = { + NAME: CONFIG.chainName, + RAILGUN_ADDRESS: CONFIG.railgunAddress as `0x${string}`, + GLOBAL_START_BLOCK: startBlock, + CHAIN_ID: BigInt(CONFIG.chainId), + RELAY_ADAPT_ADDRESS: (CONFIG.relayAdaptAddress || CONFIG.railgunAddress) as `0x${string}`, + WETH: WETH_PLACEHOLDER as `0x${string}`, + FEE_BASIS_POINTS: 25n, // 0.25% fee + }; + + console.log(' โœ“ Network configuration:'); + console.log(` Chain ID: ${devnetConfig.CHAIN_ID}`); + console.log(` RAILGUN: ${devnetConfig.RAILGUN_ADDRESS}`); + console.log(` RelayAdapt: ${devnetConfig.RELAY_ADAPT_ADDRESS}`); + console.log(` Start Block: ${devnetConfig.GLOBAL_START_BLOCK}\n`); + + // 2. Create provider adapter + console.log(' ๐Ÿ”Œ Creating provider adapter...'); + const providerAdapter = new EthersProviderAdapter(provider); + console.log(' โœ“ Ethers provider adapter created\n'); + + // 3. Create indexer + console.log(' ๐Ÿ“‡ Creating RAILGUN indexer...'); + indexer = await createRailgunIndexer({ + network: devnetConfig, + provider: providerAdapter, + startBlock: devnetConfig.GLOBAL_START_BLOCK, + }); + console.log(' โœ“ Indexer created\n'); + + // 4. Create Alice's account + console.log(' ๐Ÿ‘ค Creating Alice\'s RAILGUN account...'); + const aliceMnemonic = 'test test test test test test test test test test test junk'; + const aliceSigner = new EthersSignerAdapter(signerA); + + aliceAccount = await createRailgunAccount({ + credential: { + type: 'mnemonic', + mnemonic: aliceMnemonic, + accountIndex: 0, + }, + indexer, + }); + + // Set signer for shield operations + (aliceAccount as any)._internal.signer = aliceSigner; + + const aliceRailgunAddress = await aliceAccount.getRailgunAddress(); + console.log(` โœ“ Alice RAILGUN address: ${aliceRailgunAddress}\n`); + + // 5. Create Bob's account + console.log(' ๐Ÿ‘ค Creating Bob\'s RAILGUN account...'); + const bobMnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + const bobSigner = new EthersSignerAdapter(signerB); + + bobAccount = await createRailgunAccount({ + credential: { + type: 'mnemonic', + mnemonic: bobMnemonic, + accountIndex: 0, + }, + indexer, + }); + + // Set signer for operations + (bobAccount as any)._internal.signer = bobSigner; + + const bobRailgunAddress = await bobAccount.getRailgunAddress(); + console.log(` โœ“ Bob RAILGUN address: ${bobRailgunAddress}\n`); + + console.log('๐ŸŽ‰ Kohaku RAILGUN SDK Initialized'); +} + +async function handleShield() { + console.log('๐Ÿ”’ Step 3: Shield - Alice deposits tokens into privacy pool'); + + const symbol = await tokenContract.symbol(); + const balanceABefore = await tokenContract.balanceOf(signerA.address); + + console.log(` Before Shield:`); + console.log(` Alice public balance: ${ethers.formatEther(balanceABefore)} ${symbol}`); + console.log(` Alice private balance: 0 ${symbol}\n`); + + // 1. Approve RAILGUN to spend tokens + console.log(` ๐Ÿ“ค Approving RAILGUN to spend ${ethers.formatEther(CONFIG.testAmount)} ${symbol}...`); + const approveTx = await tokenContract.connect(signerA).approve(CONFIG.railgunAddress, CONFIG.testAmount); + await approveTx.wait(); + console.log(` โœ“ Approval confirmed\n`); + + // 2. Generate shield transaction using Kohaku + console.log(' ๐Ÿ“ Generating Shield transaction with Kohaku...'); + const shieldTxData = await aliceAccount.shield( + tokenAddress as `0x${string}`, + CONFIG.testAmount + ); + + console.log(' โœ“ Shield transaction generated\n'); + + // 3. Submit transaction + console.log(' ๐Ÿ“ค Submitting Shield transaction...'); + const shieldTx = await signerA.sendTransaction({ + ...shieldTxData, // Use spread operator like in Kohaku tests + gasLimit: 6000000n, + }); + + console.log(` โณ Waiting for confirmation (tx: ${shieldTx.hash})...`); + const shieldReceipt = await shieldTx.wait(); + + if (shieldReceipt!.status === 0) { + throw new Error('Shield transaction reverted'); + } + + console.log(` โœ“ Shield confirmed (block: ${shieldReceipt!.blockNumber})\n`); + + // 4. Sync indexer to process the shield event + console.log(' ๐Ÿ”„ Syncing indexer to process Shield event...'); + const currentBlock = await provider.getBlockNumber(); + + if (indexer.sync) { + await indexer.sync({ toBlock: currentBlock, logProgress: false }); + } else { + console.log(' โš ๏ธ No sync function available, events will be processed on demand\n'); + } + + console.log(' โœ“ Indexer synced\n'); + + // 5. Check balance + console.log(' โณ Waiting for balance to update...'); + let privateBalance = 0n; + let attempts = 0; + const maxAttempts = 15; + + while (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 2000)); + attempts++; + + try { + privateBalance = await aliceAccount.getBalance(tokenAddress as `0x${string}`); + if (privateBalance > 0n) { + console.log(` โœ… Balance synced in ${attempts * 2}s\n`); + break; + } + console.log(` ๐Ÿ” Attempt ${attempts}: Balance = ${privateBalance}`); + } catch (error: any) { + console.log(` โš ๏ธ Attempt ${attempts}: ${error.message}`); + } + } + + if (privateBalance === 0n) { + throw new Error(`Failed to sync private balance after ${maxAttempts * 2}s`); + } + + const balanceAAfter = await tokenContract.balanceOf(signerA.address); + console.log(` After Shield:`); + console.log(` Alice public balance: ${ethers.formatEther(balanceAAfter)} ${symbol}`); + console.log(` Alice private balance: ${ethers.formatEther(privateBalance)} ${symbol} โœจ\n`); + + console.log(' ๐Ÿ” On-chain visible: "Someone deposited 500 tokens"'); + console.log(' ๐Ÿ™ˆ Hidden: Who deposited (Alice)\n'); +} + +async function handleTransfer() { + console.log('๐Ÿ”„ Step 4: Transfer - Alice sends tokens to Bob privately'); + + // Sync indexer before transfer to ensure latest state + console.log(' ๐Ÿ”„ Pre-Transfer: Syncing indexer to latest block...'); + const latestBlock = await provider.getBlockNumber(); + if (indexer.sync) { + await indexer.sync({ toBlock: latestBlock, logProgress: false }); + } + console.log(` โœ“ Indexer synced to block ${latestBlock}\n`); + + const symbol = await tokenContract.symbol(); + const bobRailgunAddress = await bobAccount.getRailgunAddress(); + + console.log(` ๐Ÿ“ Generating Transfer transaction...`); + console.log(` Amount: ${ethers.formatEther(CONFIG.transferAmount)} ${symbol}`); + console.log(` To: ${bobRailgunAddress}\n`); + + // Generate transfer transaction (includes ZK proof generation) + console.log(' โณ Generating ZK proof ...\n'); + const transferTxData = await aliceAccount.transfer( + tokenAddress as `0x${string}`, + CONFIG.transferAmount, + bobRailgunAddress as `0x${string}` + ); + + console.log(' โœ“ Transfer transaction generated\n'); + + // Submit transaction + console.log(' ๐Ÿ“ค Submitting Transfer transaction...'); + const transferTx = await signerA.sendTransaction({ + ...transferTxData, // Use spread operator like in Kohaku tests + gasLimit: 6000000n, + }); + + console.log(` โณ Waiting for confirmation (tx: ${transferTx.hash})...`); + const transferReceipt = await transferTx.wait(); + + if (transferReceipt!.status === 0) { + console.error(' โŒ Transaction failed!'); + console.error(' ๐Ÿ“‹ Receipt:', JSON.stringify(transferReceipt, null, 2)); + throw new Error('Transfer transaction reverted'); + } + + console.log(` โœ“ Transfer confirmed (block: ${transferReceipt!.blockNumber})\n`); + + // Sync indexer + console.log(' ๐Ÿ”„ Syncing indexer...'); + const currentBlock = await provider.getBlockNumber(); + + if (indexer.sync) { + await indexer.sync({ toBlock: currentBlock, logProgress: false }); + } + + console.log(' โœ“ Indexer synced\n'); + + // Check balances + await new Promise(resolve => setTimeout(resolve, 3000)); + + const aliceBalance = await aliceAccount.getBalance(tokenAddress as `0x${string}`); + const bobBalance = await bobAccount.getBalance(tokenAddress as `0x${string}`); + + console.log(` After Transfer:`); + console.log(` Alice private balance: ${ethers.formatEther(aliceBalance)} ${symbol}`); + console.log(` Bob private balance: ${ethers.formatEther(bobBalance)} ${symbol} โœจ\n`); + + console.log(' ๐Ÿ” On-chain visible: "A transfer happened"'); + console.log(' ๐Ÿ™ˆ Hidden: Sender (Alice), Receiver (Bob), Amount (100)\n'); +} + +// ============================================================================ +// Step 5: Unshield (Privacy Withdrawal) +// ============================================================================ + +async function handleUnshield() { + console.log('๐Ÿ”“ Step 5: Unshield - Bob withdraws to public address'); + + const symbol = await tokenContract.symbol(); + + console.log(` ๐Ÿ“ Generating Unshield transaction...`); + console.log(` Amount: ${ethers.formatEther(CONFIG.transferAmount)} ${symbol}`); + console.log(` To: ${signerB.address}\n`); + + // Generate unshield transaction (includes ZK proof generation) + console.log(' โณ Generating ZK proof ..\n'); + const unshieldTxData = await bobAccount.unshield( + tokenAddress as `0x${string}`, + CONFIG.transferAmount, + signerB.address as `0x${string}` + ); + + console.log(' โœ“ Unshield transaction generated\n'); + + // Submit transaction + console.log(' ๐Ÿ“ค Submitting Unshield transaction...'); + const unshieldTx = await signerB.sendTransaction({ + ...unshieldTxData, // Use spread operator like in Kohaku tests + gasLimit: 6000000n, + }); + + console.log(` โณ Waiting for confirmation (tx: ${unshieldTx.hash})...`); + const unshieldReceipt = await unshieldTx.wait(); + + if (unshieldReceipt!.status === 0) { + throw new Error('Unshield transaction reverted'); + } + + console.log(` โœ“ Unshield confirmed (block: ${unshieldReceipt!.blockNumber})\n`); + + // Check final balances + const balanceBAfter = await tokenContract.balanceOf(signerB.address); + + console.log(` After Unshield:`); + console.log(` Bob private balance: 0 ${symbol}`); + console.log(` Bob public balance: ${ethers.formatEther(balanceBAfter)} ${symbol} โœจ\n`); + + console.log(' ๐Ÿ” On-chain visible: "Someone withdrew 100 tokens to Bob\'s address"'); + console.log(' ๐Ÿ™ˆ Hidden: Which private account belongs to Bob\n'); +} + +async function summary() { + console.log('๐Ÿ™ˆ What is hidden:'); + console.log(' โœ— Alice deposited 500 tokens'); + console.log(' โœ— Alice sent 100 tokens to Bob'); + console.log(' โœ— Transfer amount was 100 tokens'); + console.log(' โœ— Alice still has 400 tokens in privacy pool'); + console.log(' โœ— Relationship between Alice and Bob\n'); + + console.log('โœ… RAILGUN Privacy Demo Complete (Kohaku SDK)!'); +} + +async function main() { + console.log('๐Ÿš€ RAILGUN Privacy Transaction Test (Kohaku SDK)'); + + try { + // Step 1: Setup environment (deploy ERC20, send gas fees) + await setupEnvironment(); + + // Step 2: Setup Kohaku RAILGUN SDK + await setupKohakuRailgun(); + + // Step 3: Shield - Alice deposits tokens + await handleShield(); + + // Step 4: Transfer - Alice sends to Bob + await handleTransfer(); + + // Step 5: Unshield - Bob withdraws to public address + await handleUnshield(); + + // Summary + await summary(); + + // Clean exit + process.exit(0); + } catch (error: any) { + console.error('โŒ Test Failed'); + console.error(`Error: ${error.message}`); + console.error(`Stack: ${error.stack}`); + process.exit(1); + } +} + +main().catch((error) => { + console.error('\nโŒ Unexpected Error:', error); + process.exit(1); +}); + diff --git a/devnet/scripts/deploy-test-token.sh b/devnet/scripts/deploy-test-token.sh new file mode 100755 index 0000000..fc24799 --- /dev/null +++ b/devnet/scripts/deploy-test-token.sh @@ -0,0 +1,97 @@ +#!/bin/bash +set -e + +PWD_DIR="$(cd "$(dirname "$0")/.." && pwd)" +RAILGUN_ENV_FILE="$PWD_DIR/railgun/.env.railgun" + +echo "๐Ÿช™ Deploying Test ERC20 Token" + +cd "$PWD_DIR" +source .env + +# Load RAILGUN internal configuration +if [ -f "$RAILGUN_ENV_FILE" ]; then + source "$RAILGUN_ENV_FILE" +fi + +PRIVATE_KEY=${OP_PROPOSER_PRIVATE_KEY} +RPC_URL="http://127.0.0.1:8123" + +echo "๐Ÿ“ฆ Compiling and deploying ERC20..." + +cat > /tmp/SimpleToken.sol << 'SOLIDITY' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract SimpleToken { + string public name = "MyToken"; + string public symbol = "MTK"; + uint8 public decimals = 18; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor() { + totalSupply = 1000000 * 10**18; + balanceOf[msg.sender] = totalSupply; + emit Transfer(address(0), msg.sender, totalSupply); + } + + function transfer(address to, uint256 value) public returns (bool) { + require(balanceOf[msg.sender] >= value, "Insufficient balance"); + balanceOf[msg.sender] -= value; + balanceOf[to] += value; + emit Transfer(msg.sender, to, value); + return true; + } + + function approve(address spender, uint256 value) public returns (bool) { + allowance[msg.sender][spender] = value; + emit Approval(msg.sender, spender, value); + return true; + } + + function transferFrom(address from, address to, uint256 value) public returns (bool) { + require(balanceOf[from] >= value, "Insufficient balance"); + require(allowance[from][msg.sender] >= value, "Insufficient allowance"); + balanceOf[from] -= value; + balanceOf[to] += value; + allowance[from][msg.sender] -= value; + emit Transfer(from, to, value); + return true; + } +} +SOLIDITY + +echo " Using solc to compile..." +BYTECODE=$(solc --bin --optimize /tmp/SimpleToken.sol 2>/dev/null | tail -1) + +echo " โœ“ Bytecode ready" +echo " ๐Ÿ“ค Deploying to L2..." + +DEPLOY_TX=$(cast send --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --create "$BYTECODE" \ + --json 2>&1) + +if echo "$DEPLOY_TX" | grep -q "contractAddress"; then + TOKEN_ADDRESS=$(echo "$DEPLOY_TX" | jq -r '.contractAddress') + echo " โœ… Token deployed! Token Address: $TOKEN_ADDRESS" + + if grep -q "^RAILGUN_TEST_TOKEN_ADDRESS=" "$RAILGUN_ENV_FILE"; then + sed -i.bak "s|^RAILGUN_TEST_TOKEN_ADDRESS=.*|RAILGUN_TEST_TOKEN_ADDRESS=$TOKEN_ADDRESS|" "$RAILGUN_ENV_FILE" + else + echo "RAILGUN_TEST_TOKEN_ADDRESS=$TOKEN_ADDRESS" >> "$RAILGUN_ENV_FILE" + fi + rm -f "$RAILGUN_ENV_FILE.bak" + + echo "๐ŸŽ‰ Test token deployed successfully!" +else + echo " โŒ Deployment failed!" + echo "$DEPLOY_TX" + exit 1 +fi diff --git a/devnet/scripts/run-railgun-wallet.sh b/devnet/scripts/run-railgun-wallet.sh new file mode 100755 index 0000000..55293ad --- /dev/null +++ b/devnet/scripts/run-railgun-wallet.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +PWD_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. && pwd)" +RAILGUN_ENV_FILE="$PWD_DIR/railgun/.env.railgun" + +source "$PWD_DIR/.env" +source "$RAILGUN_ENV_FILE" + +RAILGUN_SDK_IMAGE_TAG="${RAILGUN_SDK_IMAGE_TAG:-railgun-sdk:latest}" + +REQUIRED_VARS=( + "CHAIN_ID" + "L2_RPC_URL" + "RAILGUN_SMART_WALLET_ADDRESS" + "RAILGUN_TEST_TOKEN_ADDRESS" +) + +MISSING_VARS=() + +for VAR in "${REQUIRED_VARS[@]}"; do + if [ -z "${!VAR}" ]; then + MISSING_VARS+=("$VAR") + fi +done + +if [ ${#MISSING_VARS[@]} -ne 0 ]; then + echo " โŒ Missing required environment variables:" + for VAR in "${MISSING_VARS[@]}"; do + echo " - $VAR" + done + echo "" + echo " Please run ./7-run-railgain.sh first to:" + echo " 1. Deploy RAILGUN contracts" + echo " 2. Deploy test token" + echo " 3. Setup environment variables" + exit 1 +fi + +echo " โœ“ All required variables set" + +# Display configuration +echo "" +echo "๐Ÿ“Š Test Configuration:" +echo " Chain ID: $CHAIN_ID" +echo " RPC URL: $L2_RPC_URL" +echo " Contract: $RAILGUN_SMART_WALLET_ADDRESS" +echo " Token: $RAILGUN_TEST_TOKEN_ADDRESS" +echo " Deploy Block: ${RAILGUN_DEPLOY_BLOCK:-0}" + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐Ÿš€ Running tests in Docker container..." +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +docker run --rm \ + -e CHAIN_ID="$CHAIN_ID" \ + -e RPC_URL="${L2_RPC_URL/localhost/host.docker.internal}" \ + -e RAILGUN_ADDRESS="$RAILGUN_SMART_WALLET_ADDRESS" \ + -e RAILGUN_RELAY_ADAPT_ADDRESS="${RAILGUN_RELAY_ADAPT_ADDRESS}" \ + -e TOKEN_ADDRESS="$RAILGUN_TEST_TOKEN_ADDRESS" \ + -e RAILGUN_DEPLOY_BLOCK="${RAILGUN_DEPLOY_BLOCK:-0}" \ + --add-host=host.docker.internal:host-gateway \ + "$RAILGUN_SDK_IMAGE_TAG" \ No newline at end of file diff --git a/devnet/scripts/setup-cgt-function.sh b/devnet/scripts/setup-cgt-function.sh index 7ecc411..afd36fa 100755 --- a/devnet/scripts/setup-cgt-function.sh +++ b/devnet/scripts/setup-cgt-function.sh @@ -52,6 +52,7 @@ setup_cgt() { --rpc-url "$L1_RPC_URL" \ --private-key "$DEPLOYER_PRIVATE_KEY" \ --legacy \ + --jobs 1 \ --broadcast 2>&1 | tee $MOCK_OKB_OUTPUT_FILE MOCK_OKB_EXIT_CODE=$? set -e @@ -107,6 +108,7 @@ setup_cgt() { --rpc-url "$L1_RPC_URL" \ --private-key "$DEPLOYER_PRIVATE_KEY" \ --legacy \ + --jobs 1 \ --broadcast 2>&1 | tee $FORGE_OUTPUT_FILE FORGE_EXIT_CODE=$? set -e