diff --git a/.github/workflows/devnet-deploys.yml b/.github/workflows/devnet-deploys.yml index f26b349ab8f..e2d7cd3f65f 100644 --- a/.github/workflows/devnet-deploys.yml +++ b/.github/workflows/devnet-deploys.yml @@ -116,7 +116,6 @@ jobs: - uses: actions/checkout@v4 with: ref: "${{ env.GIT_COMMIT }}" - fetch-depth: 0 - uses: ./.github/ci-setup-action - uses: hashicorp/setup-terraform@v3 with: @@ -209,7 +208,6 @@ jobs: - uses: actions/checkout@v4 with: ref: "${{ env.GIT_COMMIT }}" - fetch-depth: 0 - uses: ./.github/ci-setup-action @@ -256,7 +254,6 @@ jobs: - uses: actions/checkout@v4 with: ref: "${{ env.GIT_COMMIT }}" - fetch-depth: 0 - uses: ./.github/ci-setup-action - uses: hashicorp/setup-terraform@v3 with: @@ -291,7 +288,6 @@ jobs: - uses: actions/checkout@v4 with: ref: "${{ env.GIT_COMMIT }}" - fetch-depth: 0 - uses: ./.github/ci-setup-action - uses: hashicorp/setup-terraform@v3 with: diff --git a/.github/workflows/devnet-smoke.yml b/.github/workflows/devnet-smoke.yml new file mode 100644 index 00000000000..593bf067e0b --- /dev/null +++ b/.github/workflows/devnet-smoke.yml @@ -0,0 +1,85 @@ +name: Run devnet smoke tests +on: + workflow_dispatch: + workflow_run: + workflows: + - Deploy to devnet + types: + # triggered even if the workflow fails + - completed + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + GIT_COMMIT: devnet + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AZTEC_NODE_URL: https://api.aztec.network/devnet/aztec-node-1/{{ secrets.FORK_API_KEY }} + FAUCET_URL: https://api.aztec.network/devnet/aztec-faucet/{{ secrets.FORK_API_KEY }} + ETHEREUM_HOST: https://devnet-mainnet-fork.aztec.network:8545/${{ secrets.FORK_API_KEY }} + +jobs: + setup: + uses: ./.github/workflows/setup-runner.yml + with: + username: ${{ github.event.pull_request.user.login || github.actor }} + runner_type: builder-x86 + secrets: inherit + if: ${{ github.event.workflow_run.conclusion == 'success' }} + + build: + needs: setup + runs-on: ${{ github.event.pull_request.user.login || github.actor }}-x86 + outputs: + e2e_list: ${{ steps.e2e_list.outputs.list }} + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v4 + with: + ref: "${{ env.GIT_COMMIT }}" + + - uses: ./.github/ci-setup-action + with: + concurrency_key: build-test-artifacts-${{ github.actor }} + + - name: "Build E2E Image" + timeout-minutes: 40 + run: | + earthly-ci ./yarn-project+export-e2e-test-images + + - name: Create list of devnet end-to-end jobs + id: e2e_list + run: echo "list=$(earthly ls ./yarn-project/end-to-end | grep 'devnet' | sed 's/+//' | jq -R . | jq -cs .)" >> $GITHUB_OUTPUT + + e2e: + needs: build + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + test: ${{ fromJson( needs.build.outputs.e2e_list )}} + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v4 + with: { ref: "${{ env.GIT_COMMIT }}" } + - uses: ./.github/ci-setup-action + - name: Setup and Test + timeout-minutes: 40 + uses: ./.github/ensure-tester-with-images + with: + # big machine since we're doing proving + runner_type: "64core-tester-x86" + builder_type: builder-x86 + # these are copied to the tester and expected by the earthly command below + # if they fail to copy, it will try to build them on the tester and fail + builder_images_to_copy: aztecprotocol/end-to-end:${{ env.GIT_COMMIT }} + # command to produce the images in case they don't exist + builder_command: scripts/earthly-ci ./yarn-project+export-e2e-test-images + run: | + set -eux + cd ./yarn-project/end-to-end/ + export FORCE_COLOR=1 + ../../scripts/earthly-ci -P --no-output +${{ matrix.test }} diff --git a/yarn-project/cli/src/cmds/devnet/faucet.ts b/yarn-project/cli/src/cmds/devnet/faucet.ts index 291a9c97fc2..73e9d5c0642 100644 --- a/yarn-project/cli/src/cmds/devnet/faucet.ts +++ b/yarn-project/cli/src/cmds/devnet/faucet.ts @@ -1,15 +1,33 @@ import { type EthAddress } from '@aztec/circuits.js'; import { type LogFn } from '@aztec/foundation/log'; -export async function dripFaucet(faucetUrl: string, asset: string, account: EthAddress, log: LogFn): Promise { +import { prettyPrintJSON } from '../../utils/commands.js'; + +export async function dripFaucet( + faucetUrl: string, + asset: string, + account: EthAddress, + json: boolean, + log: LogFn, +): Promise { const url = new URL(`${faucetUrl}/drip/${account.toString()}`); url.searchParams.set('asset', asset); const res = await fetch(url); if (res.status === 200) { - log(`Dripped ${asset} for ${account.toString()}`); - } else if (res.status === 429) { - log(`Rate limited when dripping ${asset} for ${account.toString()}`); + if (json) { + log(prettyPrintJSON({ ok: true })); + } else { + log(`Dripped ${asset} for ${account.toString()}`); + } } else { - log(`Failed to drip ${asset} for ${account.toString()}`); + if (json) { + log(prettyPrintJSON({ ok: false })); + } else if (res.status === 429) { + log(`Rate limited when dripping ${asset} for ${account.toString()}`); + } else { + log(`Failed to drip ${asset} for ${account.toString()}`); + } + + process.exit(1); } } diff --git a/yarn-project/cli/src/cmds/devnet/index.ts b/yarn-project/cli/src/cmds/devnet/index.ts index 6562ec3bac3..7a96323825e 100644 --- a/yarn-project/cli/src/cmds/devnet/index.ts +++ b/yarn-project/cli/src/cmds/devnet/index.ts @@ -42,9 +42,10 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .requiredOption('-u, --faucet-url ', 'Url of the faucet', 'http://localhost:8082') .requiredOption('-t, --token ', 'The asset to drip', 'eth') .requiredOption('-a, --address ', 'The Ethereum address to drip to', parseEthereumAddress) + .option('--json', 'Output the result as JSON') .action(async options => { const { dripFaucet } = await import('./faucet.js'); - await dripFaucet(options.faucetUrl, options.token, options.address, log); + await dripFaucet(options.faucetUrl, options.token, options.address, options.json, log); }); return program; diff --git a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts index a62c3e71439..c42d555cb10 100644 --- a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts +++ b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts @@ -3,6 +3,7 @@ import { createEthereumChain, createL1Clients } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { ERC20PortalManager } from '../../portal_manager.js'; +import { prettyPrintJSON } from '../../utils/commands.js'; export async function bridgeERC20( amount: bigint, @@ -14,6 +15,7 @@ export async function bridgeERC20( tokenAddress: EthAddress, portalAddress: EthAddress, mint: boolean, + json: boolean, log: LogFn, debugLogger: DebugLogger, ) { @@ -25,11 +27,20 @@ export async function bridgeERC20( const portal = await ERC20PortalManager.create(tokenAddress, portalAddress, publicClient, walletClient, debugLogger); const { secret } = await portal.prepareTokensOnL1(amount, amount, recipient, mint); - if (mint) { - log(`Minted ${amount} tokens on L1 and pushed to L2 portal`); + if (json) { + log( + prettyPrintJSON({ + claimAmount: amount, + claimSecret: secret, + }), + ); } else { - log(`Bridged ${amount} tokens to L2 portal`); + if (mint) { + log(`Minted ${amount} tokens on L1 and pushed to L2 portal`); + } else { + log(`Bridged ${amount} tokens to L2 portal`); + } + log(`claimAmount=${amount},claimSecret=${secret}\n`); + log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); } - log(`claimAmount=${amount},claimSecret=${secret}\n`); - log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); } diff --git a/yarn-project/cli/src/cmds/l1/bridge_fee_juice.ts b/yarn-project/cli/src/cmds/l1/bridge_fee_juice.ts index d86b6e67ee6..4ca44ac62cc 100644 --- a/yarn-project/cli/src/cmds/l1/bridge_fee_juice.ts +++ b/yarn-project/cli/src/cmds/l1/bridge_fee_juice.ts @@ -4,6 +4,7 @@ import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { createCompatibleClient } from '../../client.js'; import { FeeJuicePortalManager } from '../../portal_manager.js'; +import { prettyPrintJSON } from '../../utils/commands.js'; export async function bridgeL1Gas( amount: bigint, @@ -11,14 +12,16 @@ export async function bridgeL1Gas( rpcUrl: string, l1RpcUrl: string, chainId: number, + privateKey: string | undefined, mnemonic: string, mint: boolean, + json: boolean, log: LogFn, debugLogger: DebugLogger, ) { // Prepare L1 client const chain = createEthereumChain(l1RpcUrl, chainId); - const { publicClient, walletClient } = createL1Clients(chain.rpcUrl, mnemonic, chain.chainInfo); + const { publicClient, walletClient } = createL1Clients(chain.rpcUrl, privateKey ?? mnemonic, chain.chainInfo); // Prepare L2 client const client = await createCompatibleClient(rpcUrl, debugLogger); @@ -27,7 +30,19 @@ export async function bridgeL1Gas( const portal = await FeeJuicePortalManager.create(client, publicClient, walletClient, debugLogger); const { secret } = await portal.prepareTokensOnL1(amount, amount, recipient, mint); - log(`Minted ${amount} gas tokens on L1 and pushed to L2 portal`); - log(`claimAmount=${amount},claimSecret=${secret}\n`); - log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); + if (json) { + const out = { + claimAmount: amount, + claimSecret: secret, + }; + log(prettyPrintJSON(out)); + } else { + if (mint) { + log(`Minted ${amount} fee juice on L1 and pushed to L2 portal`); + } else { + log(`Bridged ${amount} fee juice to L2 portal`); + } + log(`claimAmount=${amount},claimSecret=${secret}\n`); + log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); + } } diff --git a/yarn-project/cli/src/cmds/l1/create_l1_account.ts b/yarn-project/cli/src/cmds/l1/create_l1_account.ts index b2736046b9d..81764ce5f02 100644 --- a/yarn-project/cli/src/cmds/l1/create_l1_account.ts +++ b/yarn-project/cli/src/cmds/l1/create_l1_account.ts @@ -2,19 +2,14 @@ import { type LogFn } from '@aztec/foundation/log'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; -import { stringify } from '../../utils/commands.js'; +import { prettyPrintJSON } from '../../utils/commands.js'; export function createL1Account(json: boolean, log: LogFn) { const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); if (json) { - log( - stringify({ - privateKey, - address: account.address, - }), - ); + log(prettyPrintJSON({ privateKey, address: account.address })); } else { log(`Private Key: ${privateKey}`); log(`Address: ${account.address}`); diff --git a/yarn-project/cli/src/cmds/l1/get_l1_balance.ts b/yarn-project/cli/src/cmds/l1/get_l1_balance.ts index 1ca93b6b312..43f75ae5cde 100644 --- a/yarn-project/cli/src/cmds/l1/get_l1_balance.ts +++ b/yarn-project/cli/src/cmds/l1/get_l1_balance.ts @@ -5,17 +5,37 @@ import { PortalERC20Abi } from '@aztec/l1-artifacts'; import { createPublicClient, getContract, http } from 'viem'; -export async function getL1Balance(who: EthAddress, token: EthAddress, l1RpcUrl: string, chainId: number, log: LogFn) { +import { prettyPrintJSON } from '../../utils/commands.js'; + +export async function getL1Balance( + who: EthAddress, + token: EthAddress | undefined, + l1RpcUrl: string, + chainId: number, + json: boolean, + log: LogFn, +) { const chain = createEthereumChain(l1RpcUrl, chainId); const publicClient = createPublicClient({ chain: chain.chainInfo, transport: http(chain.rpcUrl) }); - const gasL1 = getContract({ - address: token.toString(), - abi: PortalERC20Abi, - client: publicClient, - }); + let balance = 0n; + if (token) { + const gasL1 = getContract({ + address: token.toString(), + abi: PortalERC20Abi, + client: publicClient, + }); - const balance = await gasL1.read.balanceOf([who.toString()]); + balance = await gasL1.read.balanceOf([who.toString()]); + } else { + balance = await publicClient.getBalance({ + address: who.toString(), + }); + } - log(`L1 gas token balance of ${who.toString()} is ${balance.toString()}`); + if (json) { + log(prettyPrintJSON({ balance })); + } else { + log(`L1 balance of ${who.toString()} is ${balance.toString()}`); + } } diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts index 7b9913172e7..4607af02c20 100644 --- a/yarn-project/cli/src/cmds/l1/index.ts +++ b/yarn-project/cli/src/cmds/l1/index.ts @@ -101,8 +101,10 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL 'test test test test test test test test test test test junk', ) .option('--mint', 'Mint the tokens on L1', false) + .option('--l1-private-key ', 'The private key to the eth account bridging', PRIVATE_KEY) .addOption(pxeOption) .addOption(l1ChainIdOption) + .option('--json', 'Output the claim in JSON format') .action(async (amount, recipient, options) => { const { bridgeL1Gas } = await import('./bridge_fee_juice.js'); await bridgeL1Gas( @@ -111,8 +113,10 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL options.rpcUrl, options.l1RpcUrl, options.l1ChainId, + options.l1PrivateKey, options.mnemonic, options.mint, + options.json, log, debugLogger, ); @@ -137,7 +141,8 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL .addOption(l1ChainIdOption) .requiredOption('-t, --token ', 'The address of the token to bridge', parseEthereumAddress) .requiredOption('-p, --portal ', 'The address of the portal contract', parseEthereumAddress) - .option('-k, --private-key ', 'The private key to use for deployment', PRIVATE_KEY) + .option('--l1-private-key ', 'The private key to use for deployment', PRIVATE_KEY) + .option('--json', 'Output the claim in JSON format') .action(async (amount, recipient, options) => { const { bridgeERC20 } = await import('./bridge_erc20.js'); await bridgeERC20( @@ -145,11 +150,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL recipient, options.l1RpcUrl, options.l1ChainId, - options.privateKey, + options.l1PrivateKey, options.mnemonic, options.token, options.portal, options.mint, + options.json, log, debugLogger, ); @@ -172,11 +178,12 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL 'Url of the ethereum host. Chain identifiers localhost and testnet can be used', ETHEREUM_HOST, ) - .requiredOption('-t, --token ', 'The address of the token to check the balance of', parseEthereumAddress) + .option('-t, --token ', 'The address of the token to check the balance of', parseEthereumAddress) .addOption(l1ChainIdOption) + .option('--json', 'Output the balance in JSON format') .action(async (who, options) => { const { getL1Balance } = await import('./get_l1_balance.js'); - await getL1Balance(who, options.token, options.l1RpcUrl, options.l1ChainId, log); + await getL1Balance(who, options.token, options.l1RpcUrl, options.l1ChainId, options.json, log); }); return program; diff --git a/yarn-project/cli/src/cmds/pxe/create_account.ts b/yarn-project/cli/src/cmds/pxe/create_account.ts index 920dfb27dcd..0b17ff0c82d 100644 --- a/yarn-project/cli/src/cmds/pxe/create_account.ts +++ b/yarn-project/cli/src/cmds/pxe/create_account.ts @@ -5,6 +5,7 @@ import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { createCompatibleClient } from '../../client.js'; import { type IFeeOpts, printGasEstimates } from '../../fees.js'; +import { prettyPrintJSON } from '../../utils/commands.js'; export async function createAccount( rpcUrl: string, @@ -14,6 +15,7 @@ export async function createAccount( skipInitialization: boolean, wait: boolean, feeOpts: IFeeOpts, + json: boolean, debugLogger: DebugLogger, log: LogFn, ) { @@ -26,16 +28,29 @@ export async function createAccount( const account = getSchnorrAccount(client, privateKey, deriveSigningKey(privateKey), salt); const { address, publicKeys, partialAddress } = account.getCompleteAddress(); - log(`\nNew account:\n`); - log(`Address: ${address.toString()}`); - log(`Public key: 0x${publicKeys.toString()}`); - if (printPK) { - log(`Private key: ${privateKey.toString()}`); + const out: Record = {}; + if (json) { + out.address = address; + out.publicKey = publicKeys; + if (printPK) { + out.privateKey = privateKey; + } + out.partialAddress = partialAddress; + out.salt = salt; + out.initHash = account.getInstance().initializationHash; + out.deployer = account.getInstance().deployer; + } else { + log(`\nNew account:\n`); + log(`Address: ${address.toString()}`); + log(`Public key: 0x${publicKeys.toString()}`); + if (printPK) { + log(`Private key: ${privateKey.toString()}`); + } + log(`Partial address: ${partialAddress.toString()}`); + log(`Salt: ${salt.toString()}`); + log(`Init hash: ${account.getInstance().initializationHash.toString()}`); + log(`Deployer: ${account.getInstance().deployer.toString()}`); } - log(`Partial address: ${partialAddress.toString()}`); - log(`Salt: ${salt.toString()}`); - log(`Init hash: ${account.getInstance().initializationHash.toString()}`); - log(`Deployer: ${account.getInstance().deployer.toString()}`); let tx; let txReceipt; @@ -51,22 +66,46 @@ export async function createAccount( }; if (feeOpts.estimateOnly) { const gas = await (await account.getDeployMethod()).estimateGas({ ...sendOpts }); - printGasEstimates(feeOpts, gas, log); + if (json) { + out.fee = { + gasLimits: { + da: gas.gasLimits.daGas, + l2: gas.gasLimits.l2Gas, + }, + teardownGasLimits: { + da: gas.teardownGasLimits.daGas, + l2: gas.teardownGasLimits, + }, + }; + } else { + printGasEstimates(feeOpts, gas, log); + } } else { tx = account.deploy({ ...sendOpts }); const txHash = await tx.getTxHash(); debugLogger.debug(`Account contract tx sent with hash ${txHash}`); + out.txHash = txHash; if (wait) { - log(`\nWaiting for account contract deployment...`); + if (!json) { + log(`\nWaiting for account contract deployment...`); + } txReceipt = await tx.wait(); + out.txReceipt = { + status: txReceipt.status, + transactionFee: txReceipt.transactionFee, + }; } } } - if (tx) { - log(`Deploy tx hash: ${await tx.getTxHash()}`); - } - if (txReceipt) { - log(`Deploy tx fee: ${txReceipt.transactionFee}`); + if (json) { + log(prettyPrintJSON(out)); + } else { + if (tx) { + log(`Deploy tx hash: ${await tx.getTxHash()}`); + } + if (txReceipt) { + log(`Deploy tx fee: ${txReceipt.transactionFee}`); + } } } diff --git a/yarn-project/cli/src/cmds/pxe/index.ts b/yarn-project/cli/src/cmds/pxe/index.ts index b9a69eca05c..3bd50263e59 100644 --- a/yarn-project/cli/src/cmds/pxe/index.ts +++ b/yarn-project/cli/src/cmds/pxe/index.ts @@ -45,6 +45,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL // `options.wait` is default true. Passing `--no-wait` will set it to false. // https://github.com/tj/commander.js#other-option-types-negatable-boolean-and-booleanvalue .option('--no-wait', 'Skip waiting for the contract to be deployed. Print the hash of deployment transaction') + .option('--json', 'Emit output as json') .action(async args => { const { createAccount } = await import('./create_account.js'); const { rpcUrl, privateKey, wait, registerOnly, skipInitialization, publicDeploy } = args; @@ -56,6 +57,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL publicDeploy, wait, FeeOpts.fromCli(args, log), + args.json, debugLogger, log, ); diff --git a/yarn-project/cli/src/utils/commands.ts b/yarn-project/cli/src/utils/commands.ts index 0ab6f57e02e..378bc2c3294 100644 --- a/yarn-project/cli/src/utils/commands.ts +++ b/yarn-project/cli/src/utils/commands.ts @@ -354,14 +354,19 @@ export function parseFields(fields: string[]): Fr[] { return fields.map(parseField); } -export function stringify(data: Record): string { +/** + * Pretty prints an object as JSON + * @param data - The object to stringify + * @returns A JSON string + */ +export function prettyPrintJSON(data: Record): string { return JSON.stringify( data, (_key, val) => { if (typeof val === 'bigint') { return String(val); - } else if (val instanceof Fr || val instanceof AztecAddress || val instanceof EthAddress) { - return val.toString(); + } else if (val && typeof val === 'object' && 'toBuffer' in val) { + return '0x' + val.toBuffer().toString('hex'); } else { return val; } diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index d9dae7c94e1..4d91c75af46 100644 --- a/yarn-project/end-to-end/Earthfile +++ b/yarn-project/end-to-end/Earthfile @@ -250,3 +250,6 @@ bench-prover: ARG COMMIT_HASH DO +E2E_COMPOSE_TEST --test=bench_prover --debug="aztec:benchmarks:*,aztec:prover*,aztec:bb*,aztec:pxe*" --enable_gas=1 --compose_file=./scripts/docker-compose-no-sandbox.yml --hardware_concurrency=${HARDWARE_CONCURRENCY:-32} DO ../../+UPLOAD_LOGS --PULL_REQUEST=$PULL_REQUEST --BRANCH=$BRANCH --COMMIT_HASH=$COMMIT_HASH + +e2e-devnet-smoke: + DO +E2E_COMPOSE_TEST --test=devnet/e2e_smoke.test.ts --compose_file=scripts/docker-compose-devnet.yml diff --git a/yarn-project/end-to-end/scripts/docker-compose-devnet.yml b/yarn-project/end-to-end/scripts/docker-compose-devnet.yml new file mode 100644 index 00000000000..4f6efe441ad --- /dev/null +++ b/yarn-project/end-to-end/scripts/docker-compose-devnet.yml @@ -0,0 +1,29 @@ +version: '3' +services: + end-to-end: + image: aztecprotocol/end-to-end:${AZTEC_DOCKER_TAG:-latest} + secrets: + - ethereum-host + - aztec-node-url + - faucet-url + environment: + DEBUG: ${DEBUG:-'aztec:*'} + DEBUG_COLORS: 1 + ETHEREUM_HOST: + JOB_NAME: ${JOB_NAME:-''} + PXE_PROVER_ENABLED: ${PXE_PROVER_ENABLED:-1} + command: | + export ETHEREUM_HOST=$$(cat /var/run/secrets/ethereum-host) + export FAUCET_URL=$$(cat /var/run/secrets/faucet-url) + export AZTEC_NODE_URL=$$(cat /var/run/secrets/aztec-node-url) + ${TEST:-./src/devnet/e2e_smoke.test.ts} + volumes: + - ../log:/usr/src/yarn-project/end-to-end/log:rw + +secrets: + aztec-node-url: + environment: AZTEC_NODE_URL + ethereum-host: + environment: ETHEREUM_HOST + faucet-url: + environment: FAUCET_URL diff --git a/yarn-project/end-to-end/src/devnet/e2e_smoke.test.ts b/yarn-project/end-to-end/src/devnet/e2e_smoke.test.ts new file mode 100644 index 00000000000..a0f8cfba405 --- /dev/null +++ b/yarn-project/end-to-end/src/devnet/e2e_smoke.test.ts @@ -0,0 +1,308 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type EthAddress, + Fr, + NativeFeePaymentMethodWithClaim, + type PXE, + SignerlessWallet, + TxStatus, + type WaitOpts, + createAztecNodeClient, + createPXEClient, + fileURLToPath, + retryUntil, +} from '@aztec/aztec.js'; +import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; +import { GasSettings, deriveSigningKey } from '@aztec/circuits.js'; +import { startHttpRpcServer } from '@aztec/foundation/json-rpc/server'; +import { type DebugLogger } from '@aztec/foundation/log'; +import { promiseWithResolvers } from '@aztec/foundation/promise'; +import { GasTokenContract, TestContract } from '@aztec/noir-contracts.js'; +import { createPXERpcServer } from '@aztec/pxe'; + +import getPort from 'get-port'; +import { exec } from 'node:child_process'; +import { lookup } from 'node:dns/promises'; +import { tmpdir } from 'node:os'; +import { resolve } from 'node:path'; + +import { getACVMConfig } from '../fixtures/get_acvm_config.js'; +import { getBBConfig } from '../fixtures/get_bb_config.js'; +import { getLogger, setupPXEService } from '../fixtures/utils.js'; + +const { + AZTEC_NODE_URL, + PXE_URL, + FAUCET_URL, + AZTEC_CLI = `node ${resolve(fileURLToPath(import.meta.url), '../../../../aztec/dest/bin/index.js')}`, + ETHEREUM_HOST, + PXE_PROVER_ENABLED = '0', + USE_EMPTY_BLOCKS = '0', +} = process.env; + +const waitOpts: WaitOpts = { timeout: 3600, interval: 1 }; + +const MIN_BLOCKS_FOR_BRIDGING = 2; + +/** + * If we can successfully resolve 'host.docker.internal', then we are running in a container, and we should treat + * localhost as being host.docker.internal. + */ +export const getLocalhost = () => + lookup('host.docker.internal') + .then(() => 'host.docker.internal') + .catch(() => 'localhost'); + +describe('End-to-end tests for devnet', () => { + // eslint-disable-next-line + let pxe: PXE; + let pxeUrl: string; // needed for the CLI + let logger: DebugLogger; + let l1ChainId: number; + let feeJuiceL1: EthAddress; + let teardown: () => void | Promise; + + beforeAll(async () => { + logger = getLogger(); + + if (!ETHEREUM_HOST) { + throw new Error('ETHEREUM_HOST must be set'); + } + + if (!AZTEC_CLI) { + throw new Error('AZTEC_CLI must be set'); + } + + if (!FAUCET_URL) { + throw new Error('FAUCET_URL must be set'); + } + + logger.info(`Using AZTEC_CLI: ${AZTEC_CLI}`); + + if (AZTEC_NODE_URL) { + logger.info(`Using AZTEC_NODE_URL: ${AZTEC_NODE_URL}`); + const node = createAztecNodeClient(AZTEC_NODE_URL); + const bbConfig = await getBBConfig(logger); + const acvmConfig = await getACVMConfig(logger); + const svc = await setupPXEService(node, { + ...bbConfig, + ...acvmConfig, + proverEnabled: ['1', 'true'].includes(PXE_PROVER_ENABLED!), + }); + pxe = svc.pxe; + + const nodeInfo = await pxe.getNodeInfo(); + const pxeInfo = await pxe.getPXEInfo(); + + expect(nodeInfo.protocolContractAddresses.classRegisterer).toEqual( + pxeInfo.protocolContractAddresses.classRegisterer, + ); + expect(nodeInfo.protocolContractAddresses.instanceDeployer).toEqual( + pxeInfo.protocolContractAddresses.instanceDeployer, + ); + expect(nodeInfo.protocolContractAddresses.gasToken).toEqual(pxeInfo.protocolContractAddresses.gasToken); + expect(nodeInfo.protocolContractAddresses.keyRegistry).toEqual(pxeInfo.protocolContractAddresses.keyRegistry); + expect(nodeInfo.protocolContractAddresses.multiCallEntrypoint).toEqual( + pxeInfo.protocolContractAddresses.multiCallEntrypoint, + ); + + const port = await getPort(); + const localhost = await getLocalhost(); + pxeUrl = `http://${localhost}:${port}`; + // start a server for the CLI to talk to + const server = startHttpRpcServer('pxe', pxe, createPXERpcServer, port); + + teardown = async () => { + const { promise, resolve, reject } = promiseWithResolvers(); + server.close(e => (e ? reject(e) : resolve())); + await promise; + + await svc.teardown(); + await bbConfig?.cleanup(); + await acvmConfig?.cleanup(); + }; + } else if (PXE_URL) { + logger.info(`Using PXE_URL: ${PXE_URL}`); + pxe = createPXEClient(PXE_URL); + pxeUrl = PXE_URL; + teardown = () => {}; + } else { + throw new Error('AZTEC_NODE_URL or PXE_URL must be set'); + } + + ({ + l1ChainId, + l1ContractAddresses: { gasTokenAddress: feeJuiceL1 }, + } = await pxe.getNodeInfo()); + logger.info(`PXE instance started`); + }); + + afterAll(async () => { + await teardown(); + }); + + it('deploys an account while paying with FeeJuice', async () => { + const privateKey = Fr.random(); + const l1Account = await cli<{ privateKey: string; address: string }>('create-l1-account'); + const l2Account = getSchnorrAccount(pxe, privateKey, deriveSigningKey(privateKey), Fr.ZERO); + + await expect(getL1Balance(l1Account.address)).resolves.toEqual(0n); + await expect(getL1Balance(l1Account.address, feeJuiceL1)).resolves.toEqual(0n); + + await cli('drip-faucet', { 'faucet-url': FAUCET_URL!, token: 'eth', address: l1Account.address }); + await expect(getL1Balance(l1Account.address)).resolves.toBeGreaterThan(0n); + + await cli('drip-faucet', { 'faucet-url': FAUCET_URL!, token: 'fee_juice', address: l1Account.address }); + await expect(getL1Balance(l1Account.address, feeJuiceL1)).resolves.toBeGreaterThan(0n); + + const amount = 1_000_000_000_000n; + const { claimAmount, claimSecret } = await cli<{ claimAmount: string; claimSecret: { value: string } }>( + 'bridge-fee-juice', + [amount, l2Account.getAddress()], + { + 'l1-rpc-url': ETHEREUM_HOST!, + 'l1-chain-id': l1ChainId.toString(), + 'l1-private-key': l1Account.privateKey, + 'rpc-url': pxeUrl, + mint: true, + }, + ); + + if (['1', 'true', 'yes'].includes(USE_EMPTY_BLOCKS)) { + await advanceChainWithEmptyBlocks(pxe); + } else { + await waitForL1MessageToArrive(); + } + + const txReceipt = await l2Account + .deploy({ + fee: { + gasSettings: GasSettings.default(), + paymentMethod: new NativeFeePaymentMethodWithClaim( + l2Account.getAddress(), + BigInt(claimAmount), + Fr.fromString(claimSecret.value), + ), + }, + }) + .wait(waitOpts); + + // disabled because the CLI process doesn't exit + // const { txHash, address } = await cli<{ txHash: string; address: { value: string } }>('create-account', { + // 'private-key': privateKey, + // payment: `method=native,claimSecret=${claimSecret.value},claimAmount=${claimAmount}`, + // wait: false, + // }); + // expect(address).toEqual(l2Account.getAddress().toString()); + // const txReceipt = await retryUntil( + // async () => { + // const receipt = await pxe.getTxReceipt(txHash); + // if (receipt.status === TxStatus.PENDING) { + // return undefined; + // } + // return receipt; + // }, + // 'wait_for_l2_account', + // waitOpts.timeout, + // waitOpts.interval, + // ); + + expect(txReceipt.status).toBe(TxStatus.SUCCESS); + const feeJuice = await GasTokenContract.at( + ( + await pxe.getNodeInfo() + ).protocolContractAddresses.gasToken, + await l2Account.getWallet(), + ); + const balance = await feeJuice.methods.balance_of_public(l2Account.getAddress()).simulate(); + expect(balance).toEqual(amount - txReceipt.transactionFee!); + }); + + type OptionValue = null | undefined | boolean | { toString(): string }; + type ArgumentValue = { toString(): string }; + + function cli(cliCommand: string): Promise; + function cli(cliCommand: string, args: ArgumentValue[]): Promise; + function cli(cliCommand: string, opts: Record): Promise; + function cli(cliCommand: string, args: ArgumentValue[], opts: Record): Promise; + function cli( + cliCommand: string, + _args?: ArgumentValue[] | Record, + _opts?: Record, + ): Promise { + const { promise, resolve, reject } = promiseWithResolvers(); + const args = Array.isArray(_args) ? _args : []; + const opts = _args && !Array.isArray(_args) ? _args : typeof _opts !== 'undefined' ? _opts : {}; + + const cmdArguments = args.map(x => x.toString()); + + opts.json = true; + const cmdOptions = Object.entries(opts) + .filter((entry): entry is [string, { toString(): string }] => entry[1] !== undefined && entry[1] !== null) + .map(([key, value]) => + typeof value === 'boolean' ? (value ? `--${key}` : `--no-${key}`) : `--${key} ${value.toString()}`, + ); + + const cmd = `${AZTEC_CLI} ${cliCommand} ${cmdArguments.join(' ')} ${cmdOptions.join(' ')}`; + logger.info(`Running aztec-cli: ${cmd}`); + const child = exec(cmd, { + cwd: tmpdir(), + env: { + NODE_OPTIONS: '--no-warnings', + }, + }); + + let err = ''; + child.stderr?.on('data', data => { + logger.error(data.toString()); + err += data.toString(); + }); + + let out = ''; + child.stdout?.on('data', data => { + out += data.toString(); + }); + + child.on('error', reject); + child.on('close', (code, signal) => { + if (code === 0) { + const res = JSON.parse(out); + logger.info(`aztec-cli JSON output: ${JSON.stringify(res)}`); + resolve(res); + } else { + reject(new Error(`aztec-cli ${cliCommand} non-zero exit: code=${code} signal=${signal} ${err}`)); + } + }); + + return promise; + } + + async function getL1Balance(address: string, token?: EthAddress): Promise { + const { balance } = await cli<{ balance: string }>('get-l1-balance', [address], { + 'l1-rpc-url': ETHEREUM_HOST!, + 'l1-chain-id': l1ChainId.toString(), + token, + }); + + return BigInt(balance); + } + + async function waitForL1MessageToArrive() { + const targetBlockNumber = (await pxe.getBlockNumber()) + MIN_BLOCKS_FOR_BRIDGING; + await retryUntil(async () => (await pxe.getBlockNumber()) >= targetBlockNumber, 'wait_for_l1_message', 0, 10); + } + + async function advanceChainWithEmptyBlocks(pxe: PXE) { + const { l1ChainId, protocolVersion } = await pxe.getNodeInfo(); + const test = await TestContract.deploy( + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(l1ChainId, protocolVersion)), + ) + .send({ universalDeploy: true, skipClassRegistration: true }) + .deployed(); + + // start at 1 because deploying the contract has already mined a block + for (let i = 1; i < MIN_BLOCKS_FOR_BRIDGING; i++) { + await test.methods.get_this_address().send().wait(waitOpts); + } + } +});