diff --git a/docs/docs/developers/docs/guides/aztec-js/how_to_test.md b/docs/docs/developers/docs/guides/aztec-js/how_to_test.md index e32fdd3f466a..6e089dd86e14 100644 --- a/docs/docs/developers/docs/guides/aztec-js/how_to_test.md +++ b/docs/docs/developers/docs/guides/aztec-js/how_to_test.md @@ -5,258 +5,7 @@ sidebar_position: 8 description: Learn how to write and run tests for your Aztec.js applications. --- -In this guide we will cover how to interact with your Aztec.nr smart contracts in a testing environment to write automated tests for your apps. - -## Prerequisites - -- A compiled contract with TS interface (read [how to compile](../smart_contracts/how_to_compile_contract.md)) -- Your sandbox running (read [getting started](../../../getting_started_on_sandbox.md)) - -## Create TS file and install libraries - -Pick where you'd like your tests to live and create a Typescript project. - -You will need to install Aztec.js: - -```bash -yarn add @aztec/aztecjs -``` - -You can use `aztec.js` to write assertions about transaction statuses, about chain state both public and private, and about logs. - -## Import relevant libraries - -Import `aztec.js`. This is an example of some functions and types you might need in your test: - -```typescript -import { getInitialTestAccountsData } from '@aztec/accounts/testing'; -import { AztecAddress, Fr, type PXE, TxStatus, createPXEClient, waitForPXE } from '@aztec/aztec.js'; -import { CheatCodes } from '@aztec/aztec/testing'; -``` - -You should also import the [Typescript class you generated](../smart_contracts/how_to_compile_contract.md#typescript-interfaces): - -```typescript -import { MyTestContract } from './artifacts/MyTestContract'; - -// assuming you already have a wallet with an account -const contract = MyTestContract.deploy(wallet).send({ - from: testAccount.address, -}).deployed() -``` - -## Write tests - -### Calling and sending transactions - -You can send transactions within your tests with Aztec.js. Read how to do that in these guides: - -- [Simulate a function](./how_to_simulate_function.md) -- [Send a transaction](./how_to_send_transaction.md) - -### Using debug options - -You can use the `debug` option in the `wait` method to get more information about the effects of the transaction. This includes information about new note hashes added to the note hash tree, new nullifiers, public data writes, new L2 to L1 messages, new contract information, and newly visible notes. - -This debug information will be populated in the transaction receipt. You can log it to the console or use it to make assertions about the transaction. - -```typescript -const tx = await contract.methods.my_function(param1, param2) - .send({ from: senderAddress }) - .wait({ debug: true }); - -// Access transaction effects for debugging -const txEffects = await pxe.getTxEffect(tx.txHash); -console.log('New note hashes:', txEffects.data.noteHashes); -console.log('New nullifiers:', txEffects.data.nullifiers); -console.log('Public data writes:', txEffects.data.publicDataWrites); -``` - -You can also log directly from Aztec contracts. Read [this guide](../local_env/how_to_debug.md#in-aztecnr-contracts) for some more information. - -## Cheats - -The [`CheatCodes`](../../reference/environment_reference/cheat_codes.md) class, which we used for [calculating the storage slot above](#querying-state), also includes a set of cheat methods for modifying the chain state that can be handy for testing. - -### Set next block timestamp - -Since the rollup time is dependent on what "slot" the block is included in, time can be progressed by progressing slots. -The duration of a slot is available by calling `getSlotDuration()` on the Rollup (code in Rollup.sol). - -You can then use the `warp` function on the EthCheatCodes to progress the underlying chain. - -```typescript -// Get current slot duration from the rollup contract -const rollup = getRollupContract(ethereumClient); -const slotDuration = await rollup.read.getSlotDuration(); - -// Progress time by one slot -const ethCheatCodes = new EthCheatCodes(ethereumClient); -await ethCheatCodes.warp(Date.now() / 1000 + slotDuration); -``` - - -### Examples - -#### A private call fails - -We can check that a call to a private function would fail by simulating it locally and expecting a rejection. Remember that all private function calls are only executed locally in order to preserve privacy. As an example, we can try transferring more tokens than we have, which will fail an assertion with the `Balance too low` error message. - -```typescript -const call = token.methods.transfer(recipientAddress, 200n); -await expect(call.simulate({ from: ownerAddress })).rejects.toThrow(/Balance too low/); -``` - -Under the hood, the `send()` method executes a simulation, so we can just call the usual `send().wait()` to catch the same failure. - -```typescript -const call = token.methods.transfer(recipientAddress, 200n); -await expect(call.simulate({ from: ownerAddress })).rejects.toThrow(/Balance too low/); -``` - -#### A transaction is dropped - -We can have private transactions that work fine locally, but are dropped by the sequencer when tried to be included due to an existing nullifier. In this example, we simulate two different transfers that would succeed individually, but not when both are tried to be mined. Here we need to `send()` the transaction and `wait()` for it to be mined. - -```typescript -// Create two transfers that would succeed individually -const call1 = token.methods.transfer(recipientAddress, 80n); -const call2 = token.methods.transfer(recipientAddress, 50n); - -// Prove both transactions -const provenCall1 = await call1.prove({ from: ownerAddress }); -const provenCall2 = await call2.prove({ from: ownerAddress }); - -// First one succeeds -await provenCall1.send().wait(); - -// Second one is dropped due to double-spend -await expect(provenCall2.send().wait()).rejects.toThrow(/dropped|nullifier/i); -``` - -#### A public call fails locally - -Public function calls can be caught failing locally similar to how we catch private function calls. For this example, we use a [`TokenContract` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr) instead of a private one. - -```typescript -const call = token.methods.transfer_in_public(ownerAddress, recipientAddress, 1000n, 0); -await expect(call.simulate({ from: ownerAddress })).rejects.toThrow(/underflow/); -``` - -#### A public call fails on the sequencer - -This will submit a failing call to the sequencer, who will include the transaction, but without any side effects from our application logic. Requesting the receipt for the transaction will also show it has a reverted status. - -```typescript -const ethRpcUrl = "http://localhost:8545"; - -// Set up CheatCodes for testing -const cheats = await CheatCodes.create(ethRpcUrl, pxe); - -const call = token.methods.transfer_in_public(ownerAddress, recipientAddress, 1000n, 0); -const receipt = await call.send({ from: ownerAddress }).wait({ dontThrowOnRevert: true }); - -// Check the transaction was reverted -expect(receipt.status).toEqual(TxStatus.APP_LOGIC_REVERTED); - -// Verify state wasn't modified -const ownerPublicBalanceSlot = await cheats.aztec.computeSlotInMap( - MyTokenContract.storage.public_balances.slot, - ownerAddress, -); -const balance = await pxe.getPublicStorageAt(token.address, ownerPublicBalanceSlot); -expect(balance.value).toEqual(100n); // Balance unchanged -``` - -``` -WARN Error processing tx 06dc87c4d64462916ea58426ffcfaf20017880b353c9ec3e0f0ee5fab3ea923f: Assertion failed: Balance too low. -``` - -### Querying state - -We can check private or public state directly rather than going through view-only methods, as we did in the initial example by calling `token.methods.balance().simulate()`. - -To query storage directly, you'll need to know the slot you want to access. However, when it comes to mapping types, as in most EVM languages, we'll need to calculate the slot for a given key. To do this, we'll use the [`CheatCodes`](../../reference/environment_reference/cheat_codes.md) utility class (see above): - -```typescript -const cheats = await CheatCodes.create(ethRpcUrl, pxe); - -// Calculate storage slot for a mapping entry -// The balances mapping is indexed by user address -const ownerSlot = await cheats.aztec.computeSlotInMap( - MyTokenContract.storage.balances.slot, - ownerAddress -); -``` - -#### Querying private state - -Private state in the Aztec is represented via sets of [private notes](../../concepts/storage/state_model.md#private-state). We can query the Private Execution Environment (PXE) for all notes encrypted for a given user in a contract slot. For example, this gets all notes encrypted for the `owner` user that are stored on the token contract address and on the slot that was calculated earlier. To calculate the actual balance, it extracts the `value` of each note, which is the third element, and sums them up. - -```typescript -// Sync private state first -await token.methods.sync_private_state().simulate({ from: ownerAddress }); - -// Get all notes for the owner -const notes = await pxe.getNotes({ - recipient: ownerAddress, - contractAddress: token.address, - storageSlot: ownerSlot, - scopes: [ownerAddress], -}); - -// Extract values from notes (assuming value is at index 2) -const values = notes.map(note => note.note.items[2]); -const balance = values.reduce((sum, current) => sum + current.toBigInt(), 0n); - -expect(balance).toEqual(100n); -``` - -#### Querying public state - -Public state behaves as a key-value store, much like in the EVM. We can directly query the target slot and get the result back as a buffer. Note that we use the [`TokenContract` (GitHub link)](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr) in this example, which defines a mapping of public balances on slot 6. - -```typescript -// First mint some tokens to public balance -await token.methods.mint_to_public(ownerAddress, 100n) - .send({ from: ownerAddress }) - .wait(); - -// Calculate the storage slot for public balances -const ownerPublicBalanceSlot = await cheats.aztec.computeSlotInMap( - MyTokenContract.storage.public_balances.slot, - ownerAddress, -); - -// Read the public storage value -const balance = await pxe.getPublicStorageAt(token.address, ownerPublicBalanceSlot); -expect(balance.value).toEqual(100n); -``` - -### Logs - -You can check the logs of events emitted by contracts. Contracts in Aztec can emit both encrypted and unencrypted events. - -#### Querying public logs - -We can query the PXE for the public logs emitted in the block where our transaction is mined. - -```typescript -// Emit a public event -const value = Fr.fromHexString('0xef'); -const tx = await testContract.methods.emit_public(value) - .send({ from: ownerAddress }) - .wait(); - -// Query for the logs -const filter = { - fromBlock: tx.blockNumber!, - limit: 1, // We expect 1 log -}; - -const logs = (await pxe.getPublicLogs(filter)).logs; -expect(logs[0].log.getEmittedFields()).toEqual([value]); -``` +Note: This section became completely stale so it got removed and will be rewritten. ## Further reading @@ -264,5 +13,4 @@ expect(logs[0].log.getEmittedFields()).toEqual([value]); - [How to send transactions in Aztec.js](./how_to_send_transaction.md) - [How to deploy a contract in Aztec.js](./how_to_deploy_contract.md) - [How to create an account in Aztec.js](./how_to_create_account.md) -- [Cheat codes](../../reference/environment_reference/cheat_codes.md) - [How to compile a contract](../smart_contracts/how_to_compile_contract.md). diff --git a/docs/docs/developers/docs/reference/environment_reference/cheat_codes.md b/docs/docs/developers/docs/reference/environment_reference/cheat_codes.md deleted file mode 100644 index 1d693bb3c3ae..000000000000 --- a/docs/docs/developers/docs/reference/environment_reference/cheat_codes.md +++ /dev/null @@ -1,540 +0,0 @@ ---- -title: Cheat Codes -description: Learn about cheat codes available in the Aztec development environment for testing and debugging. -tags: [sandbox] -sidebar_position: 4 ---- - -import Disclaimer from "@site/src/components/Disclaimers/\_wip_disclaimer.mdx"; - -## Introduction - -To help with testing, the sandbox is shipped with a set of cheatcodes. - -Cheatcodes allow you to change the time of the Aztec block, load certain state or more easily manipulate Ethereum instead of having to write dedicated RPC calls to anvil or hardhat. - -:::info Prerequisites -If you aren't familiar with [Anvil (Foundry)](https://book.getfoundry.sh/anvil/), we recommend reading up on that since Aztec Sandbox uses Anvil as the local Ethereum instance. -::: - -### Aims - -The guide will cover how to manipulate the state of the: - -- Ethereum blockchain; -- Aztec network. - -### Dependencies - -For this guide, the following Aztec packages are used: - -- @aztec/aztec.js - -### Initialization - -```ts -import { createPXEClient, CheatCodes } from "@aztec/aztec.js"; -const pxeRpcUrl = "http://localhost:8080"; -const ethRpcUrl = "http://localhost:8545"; -const pxe = createPXEClient(pxeRpcUrl); -const cc = await CheatCodes.create(ethRpcUrl, pxe); -``` - -There are two properties of the CheatCodes class - `eth` and `aztec` for cheatcodes relating to the Ethereum blockchain (L1) and the Aztec network (L2) respectively. - -## Ethereum related cheatcodes - -These are cheatcodes exposed from anvil/hardhat conveniently wrapped for ease of use in the Sandbox. - -### Interface - -```ts -// Fetch current block number of Ethereum -public async blockNumber(): Promise - -// Fetch chain ID of the local Ethereum instance -public async chainId(): Promise - -// Fetch current timestamp on Ethereum -public async timestamp(): Promise - -// Mine a given number of blocks on Ethereum. Mines 1 block by default -public async mine(numberOfBlocks = 1): Promise - -// Set the timestamp for the next block on Ethereum. -public async setNextBlockTimestamp(timestamp: number): Promise - -// Dumps the current Ethereum chain state to a given file. -public async dumpChainState(fileName: string): Promise - -// Loads the Ethereum chain state from a file. You may use `dumpChainState()` to save the state of the Ethereum chain to a file and later load it. -public async loadChainState(fileName: string): Promise - -// Load the value at a storage slot of a contract address on Ethereum -public async load(contract: EthAddress, slot: bigint): Promise - -// Set the value at a storage slot of a contract address on Ethereum (e.g. modify a storage variable on your portal contract or even the rollup contract). -public async store(contract: EthAddress, slot: bigint, value: bigint): Promise - -// Computes the slot value for a given map and key on Ethereum. A convenient wrapper to find the appropriate storage slot to load or overwrite the state. -public keccak256(baseSlot: bigint, key: bigint): bigint - -// Let you send transactions on Ethereum impersonating an externally owned or contract, without knowing the private key. -public async startImpersonating(who: EthAddress): Promise - -// Stop impersonating an account on Ethereum that you are currently impersonating. -public async stopImpersonating(who: EthAddress): Promise - -// Set the bytecode for a Ethereum contract -public async etch(contract: EthAddress, bytecode: `0x${string}`): Promise - -// Get the bytecode for a Ethereum contract -public async getBytecode(contract: EthAddress): Promise<`0x${string}`> -``` - -### blockNumber - -#### Function Signature - -```ts -public async blockNumber(): Promise -``` - -#### Description - -Fetches the current Ethereum block number. - -#### Example - -```ts -const blockNumber = await cc.eth.blockNumber(); -``` - -### chainId - -#### Function Signature - -```ts -public async chainId(): Promise -``` - -#### Description - -Fetches the Ethereum chain ID - -#### Example - -```ts -const chainId = await cc.eth.chainId(); -``` - -### timestamp - -#### Function Signature - -```ts -public async timestamp(): Promise -``` - -#### Description - -Fetches the current Ethereum timestamp. - -#### Example - -```ts -const timestamp = await cc.eth.timestamp(); -``` - -### mine - -#### Function Signature - -```ts -public async mine(numberOfBlocks = 1): Promise -``` - -#### Description - -Mines the specified number of blocks on Ethereum (default 1). - -#### Example - -```ts -const blockNum = await cc.eth.blockNumber(); -await cc.eth.mine(10); // mines 10 blocks -const newBlockNum = await cc.eth.blockNumber(); // = blockNum + 10. -``` - -### setNextBlockTimestamp - -#### Function Signature - -```ts -public async setNextBlockTimestamp(timestamp: number): Promise -``` - -#### Description - -Sets the timestamp (unix format in seconds) for the next mined block on Ethereum. -Time can only be set in the future. -If you set the timestamp to a time in the past, this method will throw an error. - -#### Example - -```ts -// // Set next block timestamp to 16 Aug 2023 10:54:30 GMT -await cc.eth.setNextBlockTimestamp(1692183270); -// next transaction you will do will have the timestamp as 1692183270 -``` - -### dumpChainState - -#### Function Signature - -```ts -public async dumpChainState(fileName: string): Promise -``` - -#### Description - -Dumps the current Ethereum chain state to a file. -Stores a hex string representing the complete state of the chain in a file with the provided path. Can be re-imported into a fresh/restarted instance of Anvil to reattain the same state. -When combined with `loadChainState()` cheatcode, it can be let you easily import the current state of mainnet into the Anvil instance of the sandbox. - -#### Example - -```ts -await cc.eth.dumpChainState("chain-state.json"); -``` - -### loadChainState - -#### Function Signature - -```ts -public async loadChainState(fileName: string): Promise -``` - -#### Description - -Loads the Ethereum chain state from a file which contains a hex string representing an Ethereum state. -When given a file previously written to by `cc.eth.dumpChainState()`, it merges the contents into the current chain state. Will overwrite any colliding accounts/storage slots. - -#### Example - -```ts -await cc.eth.loadChainState("chain-state.json"); -``` - -### load - -#### Function Signature - -```ts -public async load(contract: EthAddress, slot: bigint): Promise -``` - -#### Description - -Loads the value at a storage slot of a Ethereum contract. - -#### Example - -```solidity -contract LeetContract { - uint256 private leet = 1337; // slot 0 -} -``` - -```ts -const leetContractAddress = EthAddress.fromString("0x1234..."); -const value = await cc.eth.load(leetContractAddress, BigInt(0)); -console.log(value); // 1337 -``` - -### store - -#### Function Signature - -```ts -public async store(contract: EthAddress, slot: bigint, value: bigint): Promise -``` - -#### Description - -Stores the value in storage slot on a Ethereum contract. - -#### Example - -```solidity -contract LeetContract { - uint256 private leet = 1337; // slot 0 -} -``` - -```ts -const leetContractAddress = EthAddress.fromString("0x1234..."); -await cc.eth.store(leetContractAddress, BigInt(0), BigInt(1000)); -const value = await cc.eth.load(leetContractAddress, BigInt(0)); -console.log(value); // 1000 -``` - -### keccak256 - -#### Function Signature - -```ts -public keccak256(baseSlot: bigint, key: bigint): bigint -``` - -#### Description - -Computes the storage slot for a map key. - -#### Example - -```solidity -contract LeetContract { - uint256 private leet = 1337; // slot 0 - mapping(address => uint256) public balances; // base slot 1 -} -``` - -```ts -// find the storage slot for key `0xdead` in the balance map. -const address = BigInt("0x000000000000000000000000000000000000dead"); -const slot = cc.eth.keccak256(1n, address); -// store balance of 0xdead as 100 -await cc.eth.store(contractAddress, slot, 100n); -``` - -### startImpersonating - -#### Function Signature - -```ts -public async startImpersonating(who: EthAddress): Promise -``` - -#### Description - -Start impersonating an Ethereum account. -This allows you to use this address as a sender. - -#### Example - -```ts -await cc.eth.startImpersonating(EthAddress.fromString(address)); -``` - -### stopImpersonating - -#### Function Signature - -```ts -public async stopImpersonating(who: EthAddress): Promise -``` - -#### Description - -Stop impersonating an Ethereum account. -Stops an active impersonation started by startImpersonating. - -#### Example - -```ts -await cc.eth.stopImpersonating(EthAddress.fromString(address)); -``` - -### getBytecode - -#### Function Signature - -```ts -public async getBytecode(contract: EthAddress): Promise<`0x${string}`> -``` - -#### Description - -Get the bytecode for an Ethereum contract. - -#### Example - -```ts -const bytecode = await cc.eth.getBytecode(contract); // 0x6080604052348015610010... -``` - -### etch - -#### Function Signature - -```ts -public async etch(contract: EthAddress, bytecode: `0x${string}`): Promise -``` - -#### Description - -Set the bytecode for an Ethereum contract. - -#### Example - -```ts -const bytecode = `0x6080604052348015610010...`; -await cc.eth.etch(contract, bytecode); -console.log(await cc.eth.getBytecode(contract)); // 0x6080604052348015610010... -``` - -## Aztec related cheatcodes - -These are cheatcodes specific to manipulating the state of Aztec rollup. - -### Interface - -```ts -// Get the current aztec block number -public async blockNumber(): Promise - -// Set time of the next execution on aztec. It also modifies time on Ethereum for next execution and stores this time as the last rollup block on the rollup contract. -public async warp(to: number): Promise - -// Loads the value stored at the given slot in the public storage of the given contract. -public async loadPublic(who: AztecAddress, slot: Fr | bigint): Promise - -// Loads the value stored at the given slot in the private storage of the given contract. -public async loadPrivate(owner: AztecAddress, contract: AztecAddress, slot: Fr | bigint): Promise - -// Computes the slot value for a given map and key. -public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint): Fr -``` - -### blockNumber - -#### Function Signature - -```ts -public async blockNumber(): Promise -``` - -#### Description - -Get the current aztec block number. - -#### Example - -```ts -const blockNumber = await cc.aztec.blockNumber(); -``` - -### warp - -#### Function Signature - -```ts -public async warp(to: number): Promise -``` - -#### Description - -Sets the time on Ethereum and the time of the next block on Aztec. -Like with the corresponding Ethereum cheatcode, time can only be set in the future, not the past. -Otherwise, it will throw an error. - -#### Example - -```ts -const timestamp = await cc.eth.timestamp(); -const newTimestamp = timestamp + 100_000_000; -await cc.aztec.warp(newTimestamp); -// any Aztec.nr contract calls that make use of current timestamp -// and is executed in the next rollup block will now read `newTimestamp` -``` - -### computeSlotInMap - -#### Function Signature - -```ts -public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint): Fr -``` - -#### Description - -Compute storage slot for a map key. -The baseSlot is specified in the Aztec.nr contract. - -#### Example - -```rust -#[storage] -struct Storage { - balances: Map>, -} - -contract Token { - ... -} -``` - -```ts -const slot = cc.aztec.computeSlotInMap(1n, key); -``` - -### loadPublic - -#### Function Signature - -```ts -public async loadPublic(who: AztecAddress, slot: Fr | bigint): Promise -``` - -#### Description - -Loads the value stored at the given slot in the public storage of the given contract. - -Note: One Field element occupies a storage slot. Hence, structs with multiple field elements will be spread over multiple sequential slots. Using loadPublic will only load a single field of the struct (depending on the size of the attributes within it). - -#### Example - -```rust -#[storage] -struct Storage { - balances: Map>, -} - -contract Token { - ... -} -``` - -```ts -const address = AztecAddress.fromString("0x123..."); -const slot = cc.aztec.computeSlotInMap(1n, key); -const value = await cc.aztec.loadPublic(address, slot); -``` - -### loadPrivate - -#### Function Signature - -```ts -public async loadPrivate(owner: AztecAddress, contract: AztecAddress, slot: Fr | bigint): Promise -``` - -#### Description - -Loads the value stored at the given slot in the private storage of the given contract. - -Note: One Field element occupies a storage slot. Hence, structs with multiple field elements will be spread over multiple sequential slots. Using loadPublic will only load a single field of the struct (depending on the size of the attributes within it). - -#### Example - -#include_code load_private_cheatcode yarn-project/end-to-end/src/e2e_cheat_codes.test.ts typescript - -## Participate - -Keep up with the latest discussion and join the conversation in the [Aztec forum](https://discourse.aztec.network). - -You can also use the above link to request more cheatcodes. - - diff --git a/docs/docs/developers/docs/reference/environment_reference/sandbox-reference.md b/docs/docs/developers/docs/reference/environment_reference/sandbox-reference.md index 0d0be33fae76..3ae7b034ef04 100644 --- a/docs/docs/developers/docs/reference/environment_reference/sandbox-reference.md +++ b/docs/docs/developers/docs/reference/environment_reference/sandbox-reference.md @@ -91,14 +91,6 @@ P2P_LISTEN_ADDR=0.0.0.0 # The address on which the P2P service should listen fo P2P_PORT=40400 # The Port that will be used for sending & listening p2p messages (default: 40400) ``` -## Cheat Codes - -To help with testing, the sandbox is shipped with a set of cheatcodes. - -Cheatcodes allow you to change the time of the Aztec block, load certain state or more easily manipulate Ethereum instead of having to write dedicated RPC calls to anvil or hardhat. - -You can find the cheat code reference [here](./cheat_codes.md). - ## Contracts We have shipped a number of example contracts in the `@aztec/noir-contracts.js` [npm package](https://www.npmjs.com/package/@aztec/noir-contracts.js). This is included with the sandbox by default so you are able to use these contracts to test with. diff --git a/docs/docs/developers/migration_notes.md b/docs/docs/developers/migration_notes.md index 905f628b4c9c..96977fef0c69 100644 --- a/docs/docs/developers/migration_notes.md +++ b/docs/docs/developers/migration_notes.md @@ -14,6 +14,10 @@ The PXE JSON RPC Server has been removed, and PXE is now available only as a lib ## [Aztec.js] +### Removing Aztec cheatcodes + +The Aztec cheatcodes class has been removed. Its functionality can be replaced by using the `getNotes(...)` function directly available on our `TestWallet`, along with the relevant functions available on the Aztec Node interface (note that the cheatcodes were generally just a thin wrapper around the Aztec Node interface). + ### CLI Wallet commands dropped from `aztec` command The following commands used to be exposed by both the `aztec` and the `aztec-wallet` commands: diff --git a/docs/versioned_docs/version-v3.0.0-nightly.20250930/developers/docs/concepts/call_types.md b/docs/versioned_docs/version-v3.0.0-nightly.20250930/developers/docs/concepts/call_types.md index 889c5fd10cf4..aa8ea40f224e 100644 --- a/docs/versioned_docs/version-v3.0.0-nightly.20250930/developers/docs/concepts/call_types.md +++ b/docs/versioned_docs/version-v3.0.0-nightly.20250930/developers/docs/concepts/call_types.md @@ -103,7 +103,7 @@ Contract functions marked with `#[private]` can only be called privately, and as Private functions from other contracts can be called either regularly or statically by using the `.call()` and `.static_call` functions. They will also be 'executed' (i.e. proved) in the user's device, and `static_call` will fail if any state changes are attempted (like the EVM's `STATICCALL`). -```rust title="private_call" showLineNumbers +```rust title="private_call" showLineNumbers let _ = Token::at(stable_coin).burn_private(from, amount, authwit_nonce).call(&mut context); ``` > Source code: noir-projects/noir-contracts/contracts/app/lending_contract/src/main.nr#L254-L256 @@ -117,7 +117,7 @@ Since public execution can only be performed by the sequencer, public functions Since the public call is made asynchronously, any return values or side effects are not available during private execution. If the public function fails once executed, the entire transaction is reverted including state changes caused by the private part, such as new notes or nullifiers. Note that this does result in gas being spent, like in the case of the EVM. -```rust title="enqueue_public" showLineNumbers +```rust title="enqueue_public" showLineNumbers Lending::at(context.this_address()) ._deposit(AztecAddress::from_field(on_behalf_of), amount, collateral_asset) .enqueue(&mut context); @@ -129,7 +129,7 @@ It is also possible to create public functions that can _only_ be invoked by pri A common pattern is to enqueue public calls to check some validity condition on public state, e.g. that a deadline has not expired or that some public value is set. -```rust title="enqueueing" showLineNumbers +```rust title="enqueueing" showLineNumbers Router::at(ROUTER_ADDRESS).check_block_number(operation, value).call(context); ``` > Source code: noir-projects/noir-contracts/contracts/protocol/router_contract/src/utils.nr#L17-L19 @@ -140,7 +140,7 @@ For this reason we've created a canonical router contract which implements some An example of how a deadline can be checked using the router contract follows: -```rust title="call-check-deadline" showLineNumbers +```rust title="call-check-deadline" showLineNumbers privately_check_timestamp(Comparator.LT, config.deadline, &mut context); ``` > Source code: noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr#L65-L67 @@ -148,7 +148,7 @@ privately_check_timestamp(Comparator.LT, config.deadline, &mut context); `privately_check_timestamp` and `privately_check_block_number` are helper functions around the call to the router contract: -```rust title="helper_router_functions" showLineNumbers +```rust title="helper_router_functions" showLineNumbers /// Asserts that the current timestamp in the enqueued public call enqueued by `check_timestamp` satisfies /// the `operation` with respect to the `value. Preserves privacy by performing the check via the router contract. /// This conceals an address of the calling contract by setting `context.msg_sender` to the router contract address. @@ -168,7 +168,7 @@ pub fn privately_check_block_number(operation: u8, value: u32, context: &mut Pri This is what the implementation of the check timestamp functionality looks like: -```rust title="check_timestamp" showLineNumbers +```rust title="check_timestamp" showLineNumbers /// Asserts that the current timestamp in the enqueued public call satisfies the `operation` with respect /// to the `value. #[private] @@ -213,7 +213,7 @@ Since private calls are always run in a user's device, it is not possible to per Public functions in other contracts can be called both regularly and statically, just like on the EVM. -```rust title="public_call" showLineNumbers +```rust title="public_call" showLineNumbers Token::at(config.accepted_asset) .transfer_in_public(context.msg_sender(), context.this_address(), max_fee, authwit_nonce ) @@ -238,7 +238,7 @@ There are three different ways to execute an Aztec contract function using the ` This is used to get a result out of an execution, either private or public. It creates no transaction and spends no gas. The mental model is fairly close to that of [`eth_call`](#eth_call), in that it can be used to call any type of function, simulate its execution and get a result out of it. `simulate` is also the only way to run [utility functions](#utility). -```rust title="public_getter" showLineNumbers +```rust title="public_getter" showLineNumbers #[public] #[view] fn get_authorized() -> AztecAddress { @@ -248,7 +248,7 @@ fn get_authorized() -> AztecAddress { > Source code: noir-projects/noir-contracts/contracts/app/auth_contract/src/main.nr#L42-L50 -```typescript title="simulate_function" showLineNumbers +```typescript title="simulate_function" showLineNumbers const balance = await contract.methods.balance_of_public(newAccountAddress).simulate({ from: newAccountAddress }); expect(balance).toEqual(1n); ``` @@ -263,9 +263,9 @@ No correctness is guaranteed on the result of `simulate`! Correct execution is e This creates and returns a transaction request, which includes proof of correct private execution and side-effects. The request is not broadcast however, and no gas is spent. It is typically used in testing contexts to inspect transaction parameters or to check for execution failure. -```typescript title="local-tx-fails" showLineNumbers +```typescript title="local-tx-fails" showLineNumbers await expect( - claimContract.methods.claim(anotherDonationNote, donorAddress).send({ from: unrelatedAdress }).wait(), + claimContract.methods.claim(anotherDonationNote, donorAddress).send({ from: unrelatedAddress }).wait(), ).rejects.toThrow('Note does not belong to the sender'); ``` > Source code: yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts#L232-L236 @@ -275,7 +275,7 @@ await expect( This is the same as [`prove`](#prove) except it also broadcasts the transaction and returns a receipt. This is how transactions are sent, getting them to be included in blocks and spending gas. It is similar to [`eth_sendTransaction`](#eth_sendtransaction), except it also performs some work on the user's device, namely the production of the proof for the private part of the transaction. -```typescript title="send_tx" showLineNumbers +```typescript title="send_tx" showLineNumbers await contract.methods.buy_pack(seed).send({ from: firstPlayer }).wait(); ``` > Source code: yarn-project/end-to-end/src/e2e_card_game.test.ts#L116-L118 diff --git a/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr b/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr index d9fb282cdda2..5cb2b55e66c2 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_metadata.nr @@ -1,4 +1,4 @@ -use protocol_types::traits::{Packable, Serialize}; +use protocol_types::traits::{Deserialize, Packable, Serialize}; // There's temporarily quite a bit of boilerplate here because Noir does not yet support enums. This file will // eventually be simplified into something closer to: @@ -40,7 +40,7 @@ global NoteStage: NoteStageEnum = /// This represents a note in any of the three valid stages (pending same phase, pending previous phase, or settled). In /// order to access the underlying fields callers must first find the appropriate stage (e.g. via `is_settled()`) and /// then convert this into the appropriate type (e.g. via `to_settled()`). -#[derive(Eq, Serialize, Packable)] +#[derive(Deserialize, Eq, Serialize, Packable)] pub struct NoteMetadata { stage: u8, maybe_note_nonce: Field, diff --git a/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr b/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr index f423fe4e5ec9..1ffeef7427fe 100644 --- a/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr +++ b/noir-projects/aztec-nr/aztec/src/note/retrieved_note.nr @@ -1,9 +1,9 @@ use crate::note::note_metadata::NoteMetadata; -use protocol_types::{address::AztecAddress, traits::{Packable, Serialize}}; +use protocol_types::{address::AztecAddress, traits::{Deserialize, Packable, Serialize}}; /// A container of a note and the metadata required to prove its existence, regardless of whether the note is /// pending (created in the current transaction) or settled (created in a previous transaction). -#[derive(Eq, Serialize, Packable)] +#[derive(Deserialize, Eq, Serialize, Packable)] pub struct RetrievedNote { pub note: Note, pub contract_address: AztecAddress, diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index bcb2ba478365..e041575ee675 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -24,7 +24,7 @@ use dep::aztec::{ // macro support is expanded. /// A private note representing a numeric value associated to an account (e.g. a token balance). -#[derive(Eq, Serialize, Packable)] +#[derive(Deserialize, Eq, Serialize, Packable)] #[custom_note] pub struct UintNote { // The ordering of these fields is important given that it must: diff --git a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr index bf5160ac67d8..21bac7bdabd3 100644 --- a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr @@ -9,7 +9,7 @@ pub contract Claim { note_interface::NoteHash, retrieved_note::RetrievedNote, utils::compute_note_hash_for_nullification, }, - protocol_types::address::AztecAddress, + protocol_types::{address::AztecAddress, storage::map::derive_storage_slot_in_map}, state_vars::PublicImmutable, }; use dep::uint_note::uint_note::UintNote; @@ -36,19 +36,20 @@ pub contract Claim { fn claim(proof_retrieved_note: RetrievedNote, recipient: AztecAddress) { // 1) Check that the note corresponds to the target contract and belongs to the sender let target_address = storage.target_contract.read(); + let owner = proof_retrieved_note.note.get_owner(); assert( target_address == proof_retrieved_note.contract_address, "Note does not correspond to the target contract", ); - assert_eq( - proof_retrieved_note.note.get_owner(), - context.msg_sender(), - "Note does not belong to the sender", - ); + assert_eq(owner, context.msg_sender(), "Note does not belong to the sender"); // 2) Prove that the note hash exists in the note hash tree - // Note: The note has been inserted into the donation_receipts set in the Crowdfunding contract. - let note_storage_slot = Crowdfunding::storage_layout().donation_receipts.slot; + // Note: The note has been inserted into the donation_receipts set in a map under the owner address in + // the Crowdfunding contract. + let note_storage_slot = derive_storage_slot_in_map( + Crowdfunding::storage_layout().donation_receipts.slot, + owner, + ); let header = context.get_anchor_block_header(); header.prove_note_inclusion(proof_retrieved_note, note_storage_slot); diff --git a/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr index d796106f7bbb..0938b01e5fc1 100644 --- a/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/crowdfunding_contract/src/main.nr @@ -12,17 +12,22 @@ pub contract Crowdfunding { event::event_emission::emit_event_in_public, macros::{ events::event, - functions::{initializer, internal, private, public}, + functions::{initializer, internal, private, public, utility}, storage::storage, }, messages::message_delivery::MessageDelivery, protocol_types::address::AztecAddress, - state_vars::{PrivateSet, PublicImmutable}, - utils::comparison::Comparator, + state_vars::{Map, PrivateSet, PublicImmutable, storage::HasStorageSlot}, + utils::{array, comparison::Comparator}, + }; + use aztec::note::{ + constants::MAX_NOTES_PER_PAGE, note_getter_options::NoteStatus, + retrieved_note::RetrievedNote, }; use router::utils::privately_check_timestamp; use token::Token; use uint_note::uint_note::UintNote; + // docs:end:all-deps // docs:start:withdrawal-processed-event @@ -37,7 +42,7 @@ pub contract Crowdfunding { struct Storage { config: PublicImmutable, // Notes emitted to donors when they donate (can be used as proof to obtain rewards, eg in Claim contracts) - donation_receipts: PrivateSet, + donation_receipts: Map, Context>, } // docs:end:storage @@ -78,10 +83,12 @@ pub contract Crowdfunding { // contract by proving that the hash of this note exists in the note hash tree. let note = UintNote::new(amount, donor); - storage.donation_receipts.insert(note).emit( + // We don't constrain encryption because the donor is sending the note to himself. And hence by performing + // encryption incorrectly would harm himself only. + storage.donation_receipts.at(donor).insert(note).emit( &mut context, donor, - MessageDelivery.CONSTRAINED_ONCHAIN, + MessageDelivery.UNCONSTRAINED_ONCHAIN, ); } // docs:end:donate @@ -110,4 +117,34 @@ pub contract Crowdfunding { emit_event_in_public(WithdrawalProcessed { amount, who: to }, &mut context); } // docs:end:operator-withdrawals + + #[utility] + unconstrained fn get_donation_notes( + donor: AztecAddress, + page_index: u32, + ) -> BoundedVec, MAX_NOTES_PER_PAGE> { + let storage_slot = storage.donation_receipts.at(donor).get_storage_slot(); + + // We bypass Aztec.nr's higher-level abstractions here to access the metadata available only in RetrievedNote + // type that is not returned by view_notes function. We on the other hand need this metadata because we will + // be proving the note existence in the Claim contract. + let opt_notes = aztec::oracle::notes::get_notes( + storage_slot, + 0, + [], + [], + [], + [], + [], + [], + [], + [], + [], + MAX_NOTES_PER_PAGE, + page_index * MAX_NOTES_PER_PAGE, + NoteStatus.ACTIVE, + ); + + array::collapse(opt_notes) + } } diff --git a/yarn-project/aztec/src/testing/aztec_cheat_codes.ts b/yarn-project/aztec/src/testing/aztec_cheat_codes.ts deleted file mode 100644 index 82895a5de2d8..000000000000 --- a/yarn-project/aztec/src/testing/aztec_cheat_codes.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Fr } from '@aztec/foundation/fields'; -import { createLogger } from '@aztec/foundation/log'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { deriveStorageSlotInMap } from '@aztec/stdlib/hash'; -import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import type { Note, NotesFilter, UniqueNote } from '@aztec/stdlib/note'; - -/** - * A class that provides utility functions for interacting with the aztec chain. - */ -export class AztecCheatCodes { - constructor( - /** - * The test wallet or pxe to use for getting notes - */ - public testWalletOrPxe: { getNotes(filter: NotesFilter): Promise }, - /** - * The Aztec Node to use for interacting with the chain - */ - public node: AztecNode, - /** - * The logger to use for the aztec cheatcodes - */ - public logger = createLogger('aztecjs:cheat_codes'), - ) {} - - /** - * Computes the slot value for a given map and key. - * @param mapSlot - The slot of the map (specified in Aztec.nr contract) - * @param key - The key to lookup in the map - * @returns The storage slot of the value in the map - */ - public computeSlotInMap(mapSlot: Fr | bigint, key: Fr | bigint | AztecAddress): Promise { - const keyFr = typeof key === 'bigint' ? new Fr(key) : key.toField(); - return deriveStorageSlotInMap(mapSlot, keyFr); - } - - /** - * Get the current blocknumber - * @returns The current block number - */ - public async blockNumber(): Promise { - return await this.node.getBlockNumber(); - } - - /** - * Get the current timestamp - * @returns The current timestamp - */ - public async timestamp(): Promise { - const res = await this.node.getBlock(await this.blockNumber()); - return Number(res?.header.globalVariables.timestamp ?? 0); - } - - /** - * 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.node.getPublicStorageAt('latest', who, new Fr(slot)); - return storageValue; - } - - /** - * Loads the value stored at the given slot in the private storage of the given contract. - * @param contract - The address of the contract - * @param recipient - The address whose public key was used to encrypt the note - * @param slot - The storage slot to lookup - * @returns The notes stored at the given slot - */ - public async loadPrivate(recipient: AztecAddress, contract: AztecAddress, slot: Fr | bigint): Promise { - const extendedNotes = await this.testWalletOrPxe.getNotes({ - recipient, - contractAddress: contract, - storageSlot: new Fr(slot), - }); - return extendedNotes.map(extendedNote => extendedNote.note); - } -} diff --git a/yarn-project/aztec/src/testing/cheat_codes.ts b/yarn-project/aztec/src/testing/cheat_codes.ts index 6a85ee3cc242..5b372ad43b21 100644 --- a/yarn-project/aztec/src/testing/cheat_codes.ts +++ b/yarn-project/aztec/src/testing/cheat_codes.ts @@ -3,36 +3,28 @@ import { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; import type { DateProvider } from '@aztec/foundation/timer'; import type { SequencerClient } from '@aztec/sequencer-client'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import type { NotesFilter, UniqueNote } from '@aztec/stdlib/note'; - -import { AztecCheatCodes } from './aztec_cheat_codes.js'; /** * A class that provides utility functions for interacting with the chain. + * @deprecated There used to be 3 kinds of cheat codes: eth, rollup and aztec. We have nuked the Aztec ones because + * they became unused (we now have better testing tools). If you are introducing a new functionality to the cheat + * codes, please consider whether it makes sense to just introduce new utils in your tests instead. */ export class CheatCodes { constructor( /** Cheat codes for L1.*/ public eth: EthCheatCodes, - /** Cheat codes for Aztec L2. */ - public aztec: AztecCheatCodes, /** Cheat codes for the Aztec Rollup contract on L1. */ public rollup: RollupCheatCodes, ) {} - static async create( - rpcUrls: string[], - testWalletOrPxe: { getNotes(filter: NotesFilter): Promise }, - node: AztecNode, - dateProvider: DateProvider, - ): Promise { + static async create(rpcUrls: string[], node: AztecNode, dateProvider: DateProvider): Promise { const ethCheatCodes = new EthCheatCodes(rpcUrls, dateProvider); - const aztecCheatCodes = new AztecCheatCodes(testWalletOrPxe, node); const rollupCheatCodes = new RollupCheatCodes( ethCheatCodes, await node.getNodeInfo().then(n => n.l1ContractAddresses), ); - return new CheatCodes(ethCheatCodes, aztecCheatCodes, rollupCheatCodes); + return new CheatCodes(ethCheatCodes, rollupCheatCodes); } /** diff --git a/yarn-project/aztec/src/testing/index.ts b/yarn-project/aztec/src/testing/index.ts index 5978f08e02e6..a758fd4eaa8c 100644 --- a/yarn-project/aztec/src/testing/index.ts +++ b/yarn-project/aztec/src/testing/index.ts @@ -1,4 +1,3 @@ export { AnvilTestWatcher } from './anvil_test_watcher.js'; export { EthCheatCodes, RollupCheatCodes } from '@aztec/ethereum/test'; -export { AztecCheatCodes } from './aztec_cheat_codes.js'; export { CheatCodes } from './cheat_codes.js'; diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts index a102b4766ca4..17acb3597c9f 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts @@ -5,7 +5,7 @@ import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; describe('e2e_blacklist_token_contract mint', () => { const t = new BlacklistTokenContractTest('mint'); - let { asset, tokenSim, adminAddress, otherAddress, blacklistedAddress, wallet } = t; + let { asset, tokenSim, adminAddress, otherAddress, blacklistedAddress } = t; beforeAll(async () => { await t.applyBaseSnapshots(); @@ -13,7 +13,7 @@ describe('e2e_blacklist_token_contract mint', () => { await t.applyMintSnapshot(); await t.setup(); // Have to destructure again to ensure we have latest refs. - ({ asset, tokenSim, adminAddress, otherAddress, blacklistedAddress, wallet } = t); + ({ asset, tokenSim, adminAddress, otherAddress, blacklistedAddress } = t); }, 600_000); afterAll(async () => { @@ -85,21 +85,18 @@ describe('e2e_blacklist_token_contract mint', () => { describe('Mint flow', () => { it('mint_private as minter and redeem as recipient', async () => { + const balanceBefore = await asset.methods.balance_of_private(adminAddress).simulate({ from: adminAddress }); + const receipt = await asset.methods.mint_private(amount, secretHash).send({ from: adminAddress }).wait(); txHash = receipt.txHash; await t.addPendingShieldNoteToPXE(asset, adminAddress, amount, secretHash, txHash); - const receiptClaim = await asset.methods - .redeem_shield(adminAddress, amount, secret) - .send({ from: adminAddress }) - .wait(); + await asset.methods.redeem_shield(adminAddress, amount, secret).send({ from: adminAddress }).wait(); tokenSim.mintPrivate(adminAddress, amount); - // 1 note should have been created containing `amount` of tokens - const visibleNotes = await wallet.getNotes({ txHash: receiptClaim.txHash, contractAddress: asset.address }); - expect(visibleNotes.length).toBe(1); - expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); + const balanceAfter = await asset.methods.balance_of_private(adminAddress).simulate({ from: adminAddress }); + expect(balanceAfter).toBe(balanceBefore + amount); }); }); 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 index e87c9a3d88ad..1381b59a1abe 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -1,9 +1,7 @@ -import { type AztecAddress, EthAddress, Fr, type Wallet } from '@aztec/aztec.js'; -import { AnvilTestWatcher, CheatCodes, EthCheatCodes } from '@aztec/aztec/testing'; +import { EthAddress } from '@aztec/aztec.js'; +import { EthCheatCodes } from '@aztec/aztec/testing'; import { type ExtendedViemWalletClient, createExtendedL1Client } from '@aztec/ethereum'; -import { RollupContract } from '@aztec/ethereum/contracts'; import { DateProvider } from '@aztec/foundation/timer'; -import { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { Anvil } from '@viem/anvil'; import { parseEther } from 'viem'; @@ -11,8 +9,7 @@ import { mnemonicToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; import { MNEMONIC } from './fixtures/fixtures.js'; -import { mintTokensToPrivate } from './fixtures/token_utils.js'; -import { getLogger, setup, startAnvil } from './fixtures/utils.js'; +import { getLogger, startAnvil } from './fixtures/utils.js'; describe('e2e_cheat_codes', () => { describe('L1 cheatcodes', () => { @@ -136,87 +133,4 @@ describe('e2e_cheat_codes', () => { } }); }); - - describe('L2 cheatcodes', () => { - let wallet: Wallet; - let adminAddress: AztecAddress; - let cc: CheatCodes; - let teardown: () => Promise; - - let token: TokenContract; - let rollup: RollupContract; - let watcher: AnvilTestWatcher | undefined; - - beforeAll(async () => { - let deployL1ContractsValues; - ({ - teardown, - wallet, - accounts: [adminAddress], - cheatCodes: cc, - deployL1ContractsValues, - watcher, - } = await setup()); - if (watcher) { - watcher.setIsMarkingAsProven(false); - } - - rollup = RollupContract.getFromL1ContractsValues(deployL1ContractsValues); - token = await TokenContract.deploy(wallet, adminAddress, 'TokenName', 'TokenSymbol', 18) - .send({ from: adminAddress }) - .deployed(); - }); - - afterAll(() => teardown()); - - it('load public', async () => { - expect(adminAddress.toField().equals(await cc.aztec.loadPublic(token.address, 1n))).toBeTrue(); - }); - - it('load public returns 0 for non existent value', async () => { - const storageSlot = Fr.random(); - expect(Fr.ZERO.equals(await cc.aztec.loadPublic(token.address, storageSlot))).toBeTrue(); - }); - - it('load private works as expected for no notes', async () => { - const notes = await cc.aztec.loadPrivate(adminAddress, token.address, 5n); - const values = notes.map(note => note.items[0]); - const balance = values.reduce((sum, current) => sum + current.toBigInt(), 0n); - expect(balance).toEqual(0n); - }); - - it('load private', async () => { - // mint a token note and check it exists in balances. - // docs:start:load_private_cheatcode - const mintAmount = 100n; - - await mintTokensToPrivate(token, adminAddress, adminAddress, mintAmount); - await token.methods.sync_private_state().simulate({ from: adminAddress }); - - const balancesAdminSlot = await cc.aztec.computeSlotInMap(TokenContract.storage.balances.slot, adminAddress); - - // check if note was added to pending shield: - const notes = await cc.aztec.loadPrivate(adminAddress, token.address, balancesAdminSlot); - - // @note If you get pain for dinner, this guys is the reason. - // Assuming that it is still testing the token contract, you need to look at the balances, - // and then the type of note, currently a `UintNote` which stores fields: [owner, randomness, amount] - const values = notes.map(note => note.items[2]); - const balance = values.reduce((sum, current) => sum + current.toBigInt(), 0n); - expect(balance).toEqual(mintAmount); - // docs:end:load_private_cheatcode - }); - - it('markAsProven', async () => { - const { pendingBlockNumber, provenBlockNumber } = await rollup.getTips(); - expect(pendingBlockNumber).toBeGreaterThan(provenBlockNumber); - - await cc.rollup.markAsProven(); - - const { pendingBlockNumber: pendingBlockNumber2, provenBlockNumber: provenBlockNumber2 } = await rollup.getTips(); - expect(pendingBlockNumber2).toBe(provenBlockNumber2); - - // If this test fails, it is likely because the storage updated and is not updated in the cheatcodes. - }); - }); }); diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index eb8d015349ed..9a5c34dc7f87 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -1,9 +1,8 @@ -import { Fr, type Logger, type UniqueNote, deriveKeys } from '@aztec/aztec.js'; +import { Fr, type Logger, PublicKeys, deriveKeys } from '@aztec/aztec.js'; import { CheatCodes } from '@aztec/aztec/testing'; import { ClaimContract } from '@aztec/noir-contracts.js/Claim'; import { CrowdfundingContract } from '@aztec/noir-contracts.js/Crowdfunding'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; -import { TestContract } from '@aztec/noir-test-contracts.js/Test'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { computePartialAddress } from '@aztec/stdlib/contract'; import type { TestWallet } from '@aztec/test-wallet/server'; @@ -44,7 +43,7 @@ describe('e2e_crowdfunding_and_claim', () => { let claimContract: ClaimContract; let crowdfundingSecretKey; - let crowdfundingPublicKeys; + let crowdfundingPublicKeys: PublicKeys; let cheatCodes: CheatCodes; let deadline: number; // end of crowdfunding period @@ -84,6 +83,8 @@ describe('e2e_crowdfunding_and_claim', () => { .deployed(); logger.info(`Reward Token deployed to ${rewardToken.address}`); + // We deploy the Crowdfunding contract as an escrow contract (i.e. with populated public keys that make it + // a potential recipient of notes) because the donations accumulate "in it". crowdfundingSecretKey = Fr.random(); crowdfundingPublicKeys = (await deriveKeys(crowdfundingSecretKey)).publicKeys; @@ -118,24 +119,6 @@ describe('e2e_crowdfunding_and_claim', () => { await teardown(); }); - // Processes unique note such that it can be passed to a claim function of Claim contract - const processUniqueNote = (uniqueNote: UniqueNote) => { - return { - note: { - owner: AztecAddress.fromField(uniqueNote.note.items[0]), - randomness: uniqueNote.note.items[1], - value: uniqueNote.note.items[2].toBigInt(), // We convert to bigint as Fr is not serializable to U128 - }, - // eslint-disable-next-line camelcase - contract_address: uniqueNote.contractAddress, - metadata: { - stage: 3, // aztec::note::note_metadata::NoteStage::SETTLED - // eslint-disable-next-line camelcase - maybe_note_nonce: uniqueNote.noteNonce, - }, - }; - }; - it('full donor flow', async () => { const donationAmount = 1000n; @@ -148,21 +131,18 @@ describe('e2e_crowdfunding_and_claim', () => { 0, ); const witness = await wallet.createAuthWit(donor1Address, { caller: crowdfundingContract.address, action }); - const donateTxReceipt = await crowdfundingContract.methods + await crowdfundingContract.methods .donate(donationAmount) .send({ from: donor1Address, authWitnesses: [witness] }) .wait(); - // Get the notes emitted by the Crowdfunding contract and check that only 1 was emitted (the UintNote) - const notes = await wallet.getNotes({ - txHash: donateTxReceipt.txHash, - contractAddress: crowdfundingContract.address, - }); - const filteredNotes = notes.filter(x => x.contractAddress.equals(crowdfundingContract.address)); - expect(filteredNotes!.length).toEqual(1); - - // Set the UintNote in a format which can be passed to claim function - uintNote = processUniqueNote(filteredNotes![0]); + // The donor should have exactly one note + const pageIndex = 0; + const notes = await crowdfundingContract.methods + .get_donation_notes(donor1Address, pageIndex) + .simulate({ from: donor1Address }); + expect(notes.len).toEqual(1n); + uintNote = notes.storage[0]; } // 2) We claim the reward token via the Claim contract @@ -201,10 +181,9 @@ describe('e2e_crowdfunding_and_claim', () => { const donationAmount = 1000n; const donorAddress = donor2Address; - const unrelatedAdress = donor1Address; + const unrelatedAddress = donor1Address; // 1) We permit the crowdfunding contract to pull the donation amount from the donor's wallet, and we donate - const action = donationToken.methods.transfer_in_private( donorAddress, crowdfundingContract.address, @@ -212,26 +191,23 @@ describe('e2e_crowdfunding_and_claim', () => { 0, ); const witness = await wallet.createAuthWit(donorAddress, { caller: crowdfundingContract.address, action }); - const donateTxReceipt = await crowdfundingContract.methods + await crowdfundingContract.methods .donate(donationAmount) .send({ from: donorAddress, authWitnesses: [witness] }) .wait(); - // Get the notes emitted by the Crowdfunding contract and check that only 1 was emitted (the UintNote) - const notes = await wallet.getNotes({ - contractAddress: crowdfundingContract.address, - txHash: donateTxReceipt.txHash, - }); - const filtered = notes.filter(x => x.contractAddress.equals(crowdfundingContract.address)); - expect(filtered!.length).toEqual(1); - - // Set the UintNote in a format which can be passed to claim function - const anotherDonationNote = processUniqueNote(filtered![0]); + // The donor should have exactly one note + const pageIndex = 0; + const notes = await crowdfundingContract.methods + .get_donation_notes(donorAddress, pageIndex) + .simulate({ from: donorAddress }); + expect(notes.len).toEqual(1n); + const anotherDonationNote = notes.storage[0]; // 2) We try to claim the reward token via the Claim contract with the unrelated wallet // docs:start:local-tx-fails await expect( - claimContract.methods.claim(anotherDonationNote, donorAddress).send({ from: unrelatedAdress }).wait(), + claimContract.methods.claim(anotherDonationNote, donorAddress).send({ from: unrelatedAddress }).wait(), ).rejects.toThrow('Note does not belong to the sender'); // docs:end:local-tx-fails }); @@ -246,33 +222,48 @@ describe('e2e_crowdfunding_and_claim', () => { ).rejects.toThrow(); }); - it('cannot claim with existing note which was not emitted by the crowdfunding contract', async () => { - // 1) Deploy a Test contract - const testContract = await TestContract.deploy(wallet).send({ from: operatorAddress }).deployed(); - - // 2) Create a note - let note: any; - const arbitraryStorageSlot = 69; + it('cannot claim with existing note which was not emitted by a different contract', async () => { + // 1) Deploy another instance of the crowdfunding contract + let otherCrowdfundingContract: CrowdfundingContract; { - const arbitraryValue = 5n; - const receipt = await testContract.methods - .call_create_note(arbitraryValue, operatorAddress, arbitraryStorageSlot, false) - .send({ from: operatorAddress }) - .wait(); - const notes = await wallet.getNotes({ txHash: receipt.txHash, contractAddress: testContract.address }); - expect(notes.length).toEqual(1); - note = processUniqueNote(notes[0]); + const otherCrowdfundingDeployment = CrowdfundingContract.deployWithPublicKeys( + crowdfundingPublicKeys, + wallet, + donationToken.address, + operatorAddress, + deadline, + ); + + otherCrowdfundingContract = await otherCrowdfundingDeployment.send({ from: operatorAddress }).deployed(); + logger.info(`Crowdfunding contract deployed at ${otherCrowdfundingContract.address}`); } - // 3) Test the note was included - await testContract.methods - .test_note_inclusion(operatorAddress, arbitraryStorageSlot) - .send({ from: operatorAddress }) + // 2) Make a donation to get a note from the other contract + await mintTokensToPrivate(donationToken, operatorAddress, donor1Address, 1000n); + const donationAmount = 1000n; + const action = donationToken.methods.transfer_in_private( + donor1Address, + otherCrowdfundingContract.address, + donationAmount, + 0, + ); + const witness = await wallet.createAuthWit(donor1Address, { caller: otherCrowdfundingContract.address, action }); + await otherCrowdfundingContract.methods + .donate(donationAmount) + .send({ from: donor1Address, authWitnesses: [witness] }) .wait(); - // 4) Finally, check that the claim process fails + // 3) Get the donation note + const pageIndex = 0; + const notes = await otherCrowdfundingContract.methods + .get_donation_notes(donor1Address, pageIndex) + .simulate({ from: donor1Address }); + expect(notes.len).toEqual(1n); + const otherContractNote = notes.storage[0]; + + // 4) Try to claim rewards using note from other contract await expect( - claimContract.methods.claim(note, donor1Address).send({ from: operatorAddress }).wait(), + claimContract.methods.claim(otherContractNote, donor1Address).send({ from: donor1Address }).wait(), ).rejects.toThrow(); }); diff --git a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts index 2145d91ea8dd..43162ee916a7 100644 --- a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts @@ -301,8 +301,8 @@ describe('e2e_pending_note_hashes_contract', () => { // Then emit another note log with the same counter as the one above, but with value 5 const txReceipt = await deployedContract.methods.test_emit_bad_note_log(owner, sender).send({ from: owner }).wait(); - const notes = await wallet.getNotes({ txHash: txReceipt.txHash, contractAddress: deployedContract.address }); + const noteHashes = (await aztecNode.getTxEffect(txReceipt.txHash))?.data.noteHashes; - expect(notes.length).toBe(1); + expect(noteHashes!.length).toBe(1); }); }); diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 059f7cb9a705..6b95f3fa1ce7 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -447,7 +447,7 @@ async function setupFromFresh( // Only enable proving if specifically requested. pxeConfig.proverEnabled = !!opts.realProofs; const wallet = await TestWallet.create(aztecNode, pxeConfig); - const cheatCodes = await CheatCodes.create(aztecNodeConfig.l1RpcUrls, wallet, aztecNode, dateProvider); + const cheatCodes = await CheatCodes.create(aztecNodeConfig.l1RpcUrls, aztecNode, dateProvider); if (statePath) { writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig, resolver)); @@ -575,7 +575,7 @@ async function setupFromState(statePath: string, logger: Logger): Promise Promise.resolve(); logger.verbose('Populating wallet from already registered accounts...'); @@ -636,7 +636,7 @@ export async function setup( logger.verbose('Creating a pxe...'); const { wallet, teardown: pxeTeardown } = await setupPXEAndGetWallet(aztecNode!, pxeOpts, logger); - const cheatCodes = await CheatCodes.create(config.l1RpcUrls, wallet, aztecNode, dateProvider); + const cheatCodes = await CheatCodes.create(config.l1RpcUrls, aztecNode, dateProvider); if ( (opts.aztecTargetCommitteeSize && opts.aztecTargetCommitteeSize > 0) || diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 00f9bb2b1749..5232011ef431 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -666,12 +666,18 @@ export class PXE { } /** - * Gets notes registered in this PXE based on the provided filter. + * A debugging utility to get notes based on the provided filter. + * + * Note that this should not be used in production code because the structure of notes is considered to be + * an implementation detail of contracts. This is only meant to be used for debugging purposes. If you need to obtain + * note-related information in production code, please implement a custom utility function on your contract and call + * that function instead (e.g. `get_balance(owner: AztecAddress) -> u128` utility function on a Token contract). + * * @param filter - The filter to apply to the notes. * @returns The requested notes. */ public async getNotes(filter: NotesFilter): Promise { - // We need to manually trigger private state sync to have a guarantee that all the events are available. + // We need to manually trigger private state sync to have a guarantee that all the notes are available. await this.simulateUtility('sync_private_state', [], filter.contractAddress); const noteDaos = await this.noteDataProvider.getNotes(filter); @@ -1098,10 +1104,6 @@ export class PXE { return decodedEvents; } - async resetNoteSyncData() { - return await this.taggingDataProvider.resetNoteSyncData(); - } - /** * Stops the PXE's job queue. */ diff --git a/yarn-project/test-wallet/src/wallet/test_wallet.ts b/yarn-project/test-wallet/src/wallet/test_wallet.ts index 354184a59cfe..3f222e05d983 100644 --- a/yarn-project/test-wallet/src/wallet/test_wallet.ts +++ b/yarn-project/test-wallet/src/wallet/test_wallet.ts @@ -229,7 +229,17 @@ export abstract class BaseTestWallet extends BaseWallet { return this.pxe.registerAccount(secretKey, partialAddress); } - // RECENTLY ADDED TO GET RID OF PXE IN END-TO-END TESTS + /** + * A debugging utility to get notes based on the provided filter. + * + * Note that this should not be used in production code because the structure of notes is considered to be + * an implementation detail of contracts. This is only meant to be used for debugging purposes. If you need to obtain + * note-related information in production code, please implement a custom utility function on your contract and call + * that function instead (e.g. `get_balance(owner: AztecAddress) -> u128` utility function on a Token contract). + * + * @param filter - The filter to apply to the notes. + * @returns The requested notes. + */ getNotes(filter: NotesFilter): Promise { return this.pxe.getNotes(filter); } @@ -243,6 +253,11 @@ export abstract class BaseTestWallet extends BaseWallet { return this.pxe; } + /** + * Stops the internal job queue. + * + * This function is typically used when tearing down tests. + */ stop(): Promise { return this.pxe.stop(); }