diff --git a/yarn-project/end-to-end/src/cheat_codes.ts b/yarn-project/end-to-end/src/cheat_codes.ts new file mode 100644 index 00000000000..190e310a194 --- /dev/null +++ b/yarn-project/end-to-end/src/cheat_codes.ts @@ -0,0 +1,170 @@ +import { AztecAddress, CircuitsWasm, Fr } from '@aztec/circuits.js'; +import { pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { AztecRPC } from '@aztec/types'; + +const toFr = (value: Fr | bigint): Fr => { + return typeof value === 'bigint' ? new Fr(value) : value; +}; + +/** + * A class that provides utility functions for interacting with the chain. + */ +export class CheatCodes { + constructor( + /** + * The L1 cheat codes. + */ + public l1: L1CheatCodes, + /** + * The L2 cheat codes. + */ + public l2: L2CheatCodes, + ) {} + + static async create(rpcUrl: string, aztecRpc: AztecRPC): Promise { + const l1CheatCodes = new L1CheatCodes(rpcUrl); + const l2CheatCodes = new L2CheatCodes(aztecRpc, await CircuitsWasm.get(), l1CheatCodes); + return new CheatCodes(l1CheatCodes, l2CheatCodes); + } +} + +/** + * A class that provides utility functions for interacting with the L1 chain. + */ +class L1CheatCodes { + constructor( + /** + * The RPC client to use for interacting with the chain + */ + public rpcUrl: string, + /** + * The logger to use for the l1 cheatcodes + */ + public logger = createDebugLogger('aztec:cheat_codes:l1'), + ) {} + + async rpcCall(method: string, params: any[]) { + const paramsString = JSON.stringify(params); + const content = { + body: `{"jsonrpc":"2.0", "method": "${method}", "params": ${paramsString}, "id": 1}`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }; + return await (await fetch(this.rpcUrl, content)).json(); + } + + /** + * Get the current blocknumber + * @returns The current block number + */ + public async blockNumber(): Promise { + const res = await this.rpcCall('eth_blockNumber', []); + return parseInt(res.result, 16); + } + + /** + * Get the current chainId + * @returns The current chainId + */ + public async chainId(): Promise { + const res = await this.rpcCall('eth_chainId', []); + return parseInt(res.result, 16); + } + + /** + * Get the current timestamp + * @returns The current timestamp + */ + public async timestamp(): Promise { + const res = await this.rpcCall('eth_getBlockByNumber', ['latest', true]); + return parseInt(res.result.timestamp, 16); + } + + /** + * Get the current chainId + * @param numberOfBlocks - The number of blocks to mine + * @returns The current chainId + */ + public async mine(numberOfBlocks = 1): Promise { + const res = await this.rpcCall('anvil_mine', [numberOfBlocks]); + if (res.error) throw new Error(`Error mining: ${res.error.message}`); + this.logger(`Mined ${numberOfBlocks} blocks`); + } + + /** + * Set the next block timestamp + * @param timestamp - The timestamp to set the next block to + */ + public async setNextBlockTimestamp(timestamp: number): Promise { + const res = await this.rpcCall('anvil_setNextBlockTimestamp', [timestamp]); + if (res.error) throw new Error(`Error setting next block timestamp: ${res.error.message}`); + this.logger(`Set next block timestamp to ${timestamp}`); + } + + // Good basis for the remaining functions: + // https://github.com/foundry-rs/foundry/blob/master/anvil/core/src/eth/mod.rs +} + +/** + * A class that provides utility functions for interacting with the L2 chain. + */ +class L2CheatCodes { + constructor( + /** + * The RPC client to use for interacting with the chain + */ + public aztecRpc: AztecRPC, + /** + * The circuits wasm module used for pedersen hashing + */ + public wasm: CircuitsWasm, + /** + * The L1 cheat codes. + */ + public l1: L1CheatCodes, + /** + * The logger to use for the l2 cheatcodes + */ + public logger = createDebugLogger('aztec:cheat_codes:l2'), + ) {} + + /** + * Computes the slot value for a given map and key. + * @param baseSlot - The base slot of the map (specified in noir contract) + * @param key - The key to lookup in the map + * @returns The storage slot of the value in the map + */ + public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint): Fr { + // Based on `at` function in + // aztec3-packages/yarn-project/noir-contracts/src/contracts/noir-aztec/src/state_vars/map.nr + return Fr.fromBuffer( + pedersenPlookupCommitInputs( + this.wasm, + [toFr(baseSlot), toFr(key)].map(f => f.toBuffer()), + ), + ); + } + + /** + * Get the current blocknumber + * @returns The current block number + */ + public async blockNumber(): Promise { + return await this.aztecRpc.getBlockNum(); + } + + /** + * Loads the value stored at the given slot in the public storage of the given contract. + * @param who - The address of the contract + * @param slot - The storage slot to lookup + * @returns The value stored at the given slot + */ + public async loadPublic(who: AztecAddress, slot: Fr | bigint): Promise { + const storageValue = await this.aztecRpc.getPublicStorageAt(who, toFr(slot)); + if (storageValue === undefined) { + throw new Error(`Storage slot ${slot} not found`); + } + return Fr.fromBuffer(storageValue); + } +} diff --git a/yarn-project/end-to-end/src/cross_chain/test_harness.ts b/yarn-project/end-to-end/src/cross_chain/test_harness.ts index 7ab7d5ad0db..130a8a24066 100644 --- a/yarn-project/end-to-end/src/cross_chain/test_harness.ts +++ b/yarn-project/end-to-end/src/cross_chain/test_harness.ts @@ -12,7 +12,8 @@ import { AztecRPC, TxStatus } from '@aztec/types'; import { Chain, HttpTransport, PublicClient, getContract } from 'viem'; -import { deployAndInitializeNonNativeL2TokenContracts, expectAztecStorageSlot } from '../utils.js'; +import { CheatCodes } from '../cheat_codes.js'; +import { deployAndInitializeNonNativeL2TokenContracts } from '../utils.js'; /** * A Class for testing cross chain interactions, contains common interactions @@ -27,6 +28,7 @@ export class CrossChainTestHarness { accounts: AztecAddress[], wallet: Wallet, logger: DebugLogger, + cheatCodes: CheatCodes, ): Promise { const walletClient = deployL1ContractsValues.walletClient; const publicClient = deployL1ContractsValues.publicClient; @@ -60,6 +62,7 @@ export class CrossChainTestHarness { return new CrossChainTestHarness( aztecNode, aztecRpcServer, + cheatCodes, accounts, logger, l2Contract, @@ -80,6 +83,8 @@ export class CrossChainTestHarness { public aztecNode: AztecNodeService | undefined, /** AztecRpcServer. */ public aztecRpcServer: AztecRPC, + /** CheatCodes. */ + public cc: CheatCodes, /** Accounts. */ public accounts: AztecAddress[], /** Logger. */ @@ -199,14 +204,11 @@ export class CrossChainTestHarness { } async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint, publicBalanceSlot: bigint) { - await expectAztecStorageSlot( - this.logger, - this.aztecRpcServer, - this.l2Contract, - publicBalanceSlot, - owner.toField(), - expectedBalance, + const balance = await this.cc.l2.loadPublic( + this.l2Contract.address, + this.cc.l2.computeSlotInMap(publicBalanceSlot, owner.toField()), ); + expect(balance.value).toBe(expectedBalance); } async checkEntryIsNotInOutbox(withdrawAmount: bigint, callerOnL1: EthAddress = EthAddress.ZERO): Promise { diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts new file mode 100644 index 00000000000..3d63096e345 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -0,0 +1,53 @@ +import { AztecNodeService } from '@aztec/aztec-node'; +import { AztecRPCServer } from '@aztec/aztec-rpc'; +import { AztecRPC } from '@aztec/types'; + +import { CheatCodes } from './cheat_codes.js'; +import { setup } from './utils.js'; + +describe('e2e_cheat_codes', () => { + let aztecNode: AztecNodeService | undefined; + let aztecRpcServer: AztecRPC; + + let cc: CheatCodes; + + beforeAll(async () => { + ({ aztecNode, aztecRpcServer, cheatCodes: cc } = await setup()); + }, 100_000); + + afterAll(async () => { + await aztecNode?.stop(); + if (aztecRpcServer instanceof AztecRPCServer) { + await aztecRpcServer?.stop(); + } + }); + + describe('L1 only', () => { + describe('mine', () => { + it(`mine block`, async () => { + const blockNumber = await cc.l1.blockNumber(); + await cc.l1.mine(); + expect(await cc.l1.blockNumber()).toBe(blockNumber + 1); + }); + + it.each([10, 42, 99])(`mine blocks`, async increment => { + const blockNumber = await cc.l1.blockNumber(); + await cc.l1.mine(increment); + expect(await cc.l1.blockNumber()).toBe(blockNumber + increment); + }); + }); + + it.each([100, 42, 99])('setNextBlockTimestamp', async increment => { + const blockNumber = await cc.l1.blockNumber(); + const timestamp = await cc.l1.timestamp(); + await cc.l1.setNextBlockTimestamp(timestamp + increment); + + expect(await cc.l1.timestamp()).toBe(timestamp); + + await cc.l1.mine(); + + expect(await cc.l1.blockNumber()).toBe(blockNumber + 1); + expect(await cc.l1.timestamp()).toBe(timestamp + increment); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts index bfeaf103c22..7b4e7e7a25f 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging.test.ts @@ -33,6 +33,7 @@ describe('e2e_cross_chain_messaging', () => { accounts, wallet, logger: logger_, + cheatCodes, } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( initialBalance, @@ -42,6 +43,7 @@ describe('e2e_cross_chain_messaging', () => { accounts, wallet, logger_, + cheatCodes, ); l2Contract = crossChainTestHarness.l2Contract; diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index c002cb5a410..46d20311335 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -7,7 +7,8 @@ import { DebugLogger } from '@aztec/foundation/log'; import { LendingContract } from '@aztec/noir-contracts/types'; import { AztecRPC, TxStatus } from '@aztec/types'; -import { calculateAztecStorageSlot, setup } from './utils.js'; +import { CheatCodes } from './cheat_codes.js'; +import { setup } from './utils.js'; describe('e2e_lending_contract', () => { let aztecNode: AztecNodeService | undefined; @@ -18,6 +19,8 @@ describe('e2e_lending_contract', () => { let contract: Contract; + let cc: CheatCodes; + const deployContract = async () => { logger(`Deploying L2 public contract...`); const tx = LendingContract.deploy(aztecRpcServer).send(); @@ -32,7 +35,7 @@ describe('e2e_lending_contract', () => { }; beforeEach(async () => { - ({ aztecNode, aztecRpcServer, wallet, accounts, logger } = await setup()); + ({ aztecNode, aztecRpcServer, wallet, accounts, logger, cheatCodes: cc } = await setup()); }, 100_000); afterEach(async () => { @@ -44,25 +47,23 @@ describe('e2e_lending_contract', () => { // Fetch a storage snapshot from the contract that we can use to compare between transitions. const getStorageSnapshot = async (contract: Contract, aztecNode: AztecRPC, account: Account) => { - const storageValues: { [key: string]: any } = {}; - - const readValue = async (slot: Fr) => - Fr.fromBuffer((await aztecNode.getPublicStorageAt(contract.address, slot)) ?? Buffer.alloc(0)); + const loadPublicStorageInMap = async (slot: Fr | bigint, key: Fr | bigint) => { + return await cc.l2.loadPublic(contract.address, cc.l2.computeSlotInMap(slot, key)); + }; + const storageValues: { [key: string]: any } = {}; { - const baseSlot = await calculateAztecStorageSlot(1n, Fr.ZERO); - storageValues['interestAccumulator'] = await readValue(baseSlot); - storageValues['last_updated_ts'] = await readValue(new Fr(baseSlot.value + 1n)); + const baseSlot = cc.l2.computeSlotInMap(1n, 0n); + storageValues['interestAccumulator'] = await cc.l2.loadPublic(contract.address, baseSlot); + storageValues['last_updated_ts'] = await cc.l2.loadPublic(contract.address, baseSlot.value + 1n); } const accountKey = await account.key(); - storageValues['private_collateral'] = await readValue(await calculateAztecStorageSlot(2n, accountKey)); - storageValues['public_collateral'] = await readValue( - await calculateAztecStorageSlot(2n, account.address.toField()), - ); - storageValues['private_debt'] = await readValue(await calculateAztecStorageSlot(3n, accountKey)); - storageValues['public_debt'] = await readValue(await calculateAztecStorageSlot(3n, account.address.toField())); + storageValues['private_collateral'] = await loadPublicStorageInMap(2n, accountKey); + storageValues['public_collateral'] = await loadPublicStorageInMap(2n, account.address.toField()); + storageValues['private_debt'] = await loadPublicStorageInMap(3n, accountKey); + storageValues['public_debt'] = await loadPublicStorageInMap(3n, account.address.toField()); return storageValues; }; diff --git a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts index 0c71864d547..7cc8ccd2934 100644 --- a/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_cross_chain_messaging.test.ts @@ -33,6 +33,7 @@ describe('e2e_public_cross_chain_messaging', () => { accounts, wallet, logger: logger_, + cheatCodes, } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( initialBalance, @@ -42,6 +43,7 @@ describe('e2e_public_cross_chain_messaging', () => { accounts, wallet, logger_, + cheatCodes, ); l2Contract = crossChainTestHarness.l2Contract; diff --git a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts index 2e997654436..ced0650c0a1 100644 --- a/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_to_private_messaging.test.ts @@ -29,6 +29,7 @@ describe('e2e_public_to_private_messaging', () => { accounts, wallet, logger: logger_, + cheatCodes, } = await setup(2); crossChainTestHarness = await CrossChainTestHarness.new( initialBalance, @@ -38,6 +39,7 @@ describe('e2e_public_to_private_messaging', () => { accounts, wallet, logger_, + cheatCodes, ); ethAccount = crossChainTestHarness.ethAccount; diff --git a/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts index 027a8a6a24b..37fe2327994 100644 --- a/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_token_contract.test.ts @@ -7,7 +7,8 @@ import { AztecRPC, L2BlockL2Logs, TxStatus } from '@aztec/types'; import times from 'lodash.times'; -import { expectAztecStorageSlot, setup } from './utils.js'; +import { CheatCodes } from './cheat_codes.js'; +import { setup } from './utils.js'; describe('e2e_public_token_contract', () => { let aztecNode: AztecNodeService | undefined; @@ -19,6 +20,8 @@ describe('e2e_public_token_contract', () => { let contract: PublicTokenContract; const balanceSlot = 1n; + let cc: CheatCodes; + const deployContract = async () => { logger(`Deploying L2 public contract...`); const txReceipt = await PublicTokenContract.deploy(wallet).send().wait(); @@ -37,7 +40,7 @@ describe('e2e_public_token_contract', () => { }; beforeEach(async () => { - ({ aztecNode, aztecRpcServer, accounts, wallet, logger } = await setup()); + ({ aztecNode, aztecRpcServer, accounts, wallet, logger, cheatCodes: cc } = await setup()); }, 100_000); afterEach(async () => { @@ -66,7 +69,10 @@ describe('e2e_public_token_contract', () => { const receipt = await tx.getReceipt(); expect(receipt.status).toBe(TxStatus.MINED); - await expectAztecStorageSlot(logger, aztecRpcServer, contract, balanceSlot, recipient.toField(), mintAmount); + + const balance = await cc.l2.loadPublic(contract.address, cc.l2.computeSlotInMap(balanceSlot, recipient.toField())); + expect(balance.value).toBe(mintAmount); + await expectLogsFromLastBlockToBe(['Coins minted']); }, 45_000); @@ -89,7 +95,9 @@ describe('e2e_public_token_contract', () => { expect(receipts.map(r => r.status)).toEqual(times(3, () => TxStatus.MINED)); expect(receipts.map(r => r.blockNumber)).toEqual(times(3, () => receipts[0].blockNumber)); - await expectAztecStorageSlot(logger, aztecRpcServer, contract, balanceSlot, recipient.toField(), mintAmount * 3n); + const balance = await cc.l2.loadPublic(contract.address, cc.l2.computeSlotInMap(balanceSlot, recipient.toField())); + expect(balance.value).toBe(mintAmount * 3n); + await expectLogsFromLastBlockToBe(['Coins minted', 'Coins minted', 'Coins minted']); }, 60_000); }); diff --git a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts index b8f116f86fd..200239520b2 100644 --- a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts @@ -10,6 +10,7 @@ import { AztecRPC, TxStatus } from '@aztec/types'; import { getContract, parseEther } from 'viem'; +import { CheatCodes } from './cheat_codes.js'; import { CrossChainTestHarness } from './cross_chain/test_harness.js'; import { delay, deployAndInitializeNonNativeL2TokenContracts, setup } from './utils.js'; @@ -31,6 +32,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { let wallet: Wallet; let accounts: AztecAddress[]; let logger: DebugLogger; + let cheatCodes: CheatCodes; let ethAccount: EthAddress; let ownerAddress: AztecAddress; @@ -47,7 +49,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { beforeEach(async () => { let deployL1ContractsValues: DeployL1Contracts; - ({ aztecNode, aztecRpcServer, deployL1ContractsValues, accounts, logger, wallet } = await setup(2)); + ({ aztecNode, aztecRpcServer, deployL1ContractsValues, accounts, logger, wallet, cheatCodes } = await setup(2)); const walletClient = deployL1ContractsValues.walletClient; const publicClient = deployL1ContractsValues.publicClient; @@ -73,6 +75,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { daiCrossChainHarness = new CrossChainTestHarness( aztecNode, aztecRpcServer, + cheatCodes, accounts, logger, daiContracts.l2Contract, @@ -101,6 +104,7 @@ describe('uniswap_trade_on_l1_from_l2', () => { wethCrossChainHarness = new CrossChainTestHarness( aztecNode, aztecRpcServer, + cheatCodes, accounts, logger, wethContracts.l2Contract, diff --git a/yarn-project/end-to-end/src/utils.ts b/yarn-project/end-to-end/src/utils.ts index a0ae3e5f40f..fea3b754930 100644 --- a/yarn-project/end-to-end/src/utils.ts +++ b/yarn-project/end-to-end/src/utils.ts @@ -17,17 +17,15 @@ import { getL1ContractAddresses, } from '@aztec/aztec.js'; import { - CircuitsWasm, DeploymentInfo, PartialContractAddress, PrivateKey, PublicKey, getContractDeploymentInfo, } from '@aztec/circuits.js'; -import { Schnorr, pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg'; +import { Schnorr } from '@aztec/circuits.js/barretenberg'; import { DeployL1Contracts, deployL1Contract, deployL1Contracts } from '@aztec/ethereum'; import { ContractAbi } from '@aztec/foundation/abi'; -import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { Fr } from '@aztec/foundation/fields'; import { mustSucceedFetch } from '@aztec/foundation/json-rpc/client'; import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; @@ -53,6 +51,7 @@ import { } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; +import { CheatCodes } from './cheat_codes.js'; import { MNEMONIC, localAnvil } from './fixtures.js'; const { SANDBOX_URL = '' } = process.env; @@ -305,6 +304,10 @@ export async function setup(numberOfAccounts = 1): Promise<{ * Logger instance named as the current test. */ logger: DebugLogger; + /** + * The cheat codes. + */ + cheatCodes: CheatCodes; }> { const config = getConfigEnvVars(); const logger = getLogger(); @@ -323,6 +326,8 @@ export async function setup(numberOfAccounts = 1): Promise<{ const { aztecRpcServer, accounts, wallet } = await setupAztecRPCServer(numberOfAccounts, aztecNode, privKey, logger); + const cheatCodes = await CheatCodes.create(config.rpcUrl, aztecRpcServer!); + return { aztecNode, aztecRpcServer, @@ -331,6 +336,7 @@ export async function setup(numberOfAccounts = 1): Promise<{ config, wallet, logger, + cheatCodes, }; } @@ -516,57 +522,6 @@ export function delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } -/** - * Calculates the slot value of a mapping within noir. - * @param slot - The storage slot of the mapping. - * @param key - The key within the mapping. - * @returns The mapping's key. - */ -export async function calculateAztecStorageSlot(slot: bigint, key: Fr): Promise { - const wasm = await CircuitsWasm.get(); - const mappingStorageSlot = new Fr(slot); // this value is manually set in the Noir contract - - // Based on `at` function in - // aztec3-packages/yarn-project/noir-contracts/src/contracts/noir-aztec/src/state_vars/map.nr - const storageSlot = Fr.fromBuffer( - pedersenPlookupCommitInputs( - wasm, - [mappingStorageSlot, key].map(f => f.toBuffer()), - ), - ); - - return storageSlot; //.value; -} - -/** - * Check the value of a public mapping's storage slot. - * @param logger - A logger instance. - * @param aztecNode - An instance of the aztec node service. - * @param contract - The contract to check the storage slot of. - * @param slot - The mapping's storage slot. - * @param key - The mapping's key. - * @param expectedValue - The expected value of the mapping. - */ -export async function expectAztecStorageSlot( - logger: DebugLogger, - aztecRpc: AztecRPC, - contract: Contract, - slot: bigint, - key: Fr, - expectedValue: bigint, -) { - const storageSlot = await calculateAztecStorageSlot(slot, key); - const storageValue = await aztecRpc.getPublicStorageAt(contract.address!, storageSlot); - if (storageValue === undefined) { - throw new Error(`Storage slot ${storageSlot} not found`); - } - - const balance = toBigIntBE(storageValue); - - logger(`Account ${key.toShortString()} balance: ${balance}`); - expect(balance).toBe(expectedValue); -} - /** * Checks the number of encrypted logs in the last block is as expected. * @param aztecNode - The instance of aztec node for retrieving the logs.