diff --git a/.circleci/config.yml b/.circleci/config.yml index e3d80cfb75b..73db5c20bda 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -970,6 +970,17 @@ jobs: command: ./scripts/cond_run_script end-to-end $JOB_NAME ./scripts/run_tests_local guides/writing_an_account_contract.test.ts working_directory: yarn-project/end-to-end + guides-dapp-testing: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_tests end-to-end guides/dapp_testing.test.ts docker-compose-e2e-sandbox.yml + e2e-canary-test: docker: - image: aztecprotocol/alpine-build-image @@ -1407,6 +1418,7 @@ workflows: - e2e-browser-sandbox: *e2e_test - aztec-rpc-sandbox: *e2e_test - guides-writing-an-account-contract: *e2e_test + - guides-dapp-testing: *e2e_test - e2e-end: requires: @@ -1437,6 +1449,7 @@ workflows: - e2e-canary-test - aztec-rpc-sandbox - guides-writing-an-account-contract + - guides-dapp-testing <<: *defaults - deploy-dockerhub: diff --git a/circuits/cpp/barretenberg/cpp/src/barretenberg/common/serialize.hpp b/circuits/cpp/barretenberg/cpp/src/barretenberg/common/serialize.hpp index 6452dcb7a4a..16d2c66e803 100644 --- a/circuits/cpp/barretenberg/cpp/src/barretenberg/common/serialize.hpp +++ b/circuits/cpp/barretenberg/cpp/src/barretenberg/common/serialize.hpp @@ -51,7 +51,8 @@ __extension__ using uint128_t = unsigned __int128; // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast, cert-dcl58-cpp) // clang-format on -template concept IntegralOrEnum = std::integral || std::is_enum_v; +template +concept IntegralOrEnum = std::integral || std::is_enum_v; namespace serialize { // Forward declare derived msgpack methods diff --git a/circuits/cpp/barretenberg/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp b/circuits/cpp/barretenberg/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp index cd8594d134a..f34a3a6dbb0 100644 --- a/circuits/cpp/barretenberg/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp +++ b/circuits/cpp/barretenberg/cpp/src/barretenberg/proof_system/circuit_builder/ultra_circuit_builder.hpp @@ -874,8 +874,8 @@ template class UltraCircuitBuilder_ : public CircuitBuilderBaseget_num_gates(); // If Sumcheck does not return an output, sumcheck verification has failed @@ -124,7 +128,11 @@ std::array UltraRecursiveVerifier_get_num_gates(); // Compute batched commitments needed for input to Gemini. @@ -150,13 +158,21 @@ std::array UltraRecursiveVerifier_(commitments.get_unshifted(), scalars_unshifted); - info("Batch mul (unshifted): num gates = ", builder->get_num_gates() - prev_num_gates, ", (total = ", builder->get_num_gates(), ")"); + info("Batch mul (unshifted): num gates = ", + builder->get_num_gates() - prev_num_gates, + ", (total = ", + builder->get_num_gates(), + ")"); prev_num_gates = builder->get_num_gates(); auto batched_commitment_to_be_shifted = GroupElement::template batch_mul(commitments.get_to_be_shifted(), scalars_to_be_shifted); - info("Batch mul (to-be-shited): num gates = ", builder->get_num_gates() - prev_num_gates, ", (total = ", builder->get_num_gates(), ")"); + info("Batch mul (to-be-shited): num gates = ", + builder->get_num_gates() - prev_num_gates, + ", (total = ", + builder->get_num_gates(), + ")"); prev_num_gates = builder->get_num_gates(); // Produce a Gemini claim consisting of: @@ -168,13 +184,21 @@ std::array UltraRecursiveVerifier_get_num_gates(); // Produce a Shplonk claim: commitment [Q] - [Q_z], evaluation zero (at random challenge z) auto shplonk_claim = Shplonk::reduce_verification(pcs_verification_key, gemini_claim, transcript); - info("Shplonk: num gates = ", builder->get_num_gates() - prev_num_gates, ", (total = ", builder->get_num_gates(), ")"); + info("Shplonk: num gates = ", + builder->get_num_gates() - prev_num_gates, + ", (total = ", + builder->get_num_gates(), + ")"); prev_num_gates = builder->get_num_gates(); // Constuct the inputs to the final KZG pairing check diff --git a/circuits/cpp/src/aztec3/circuits/kernel/private/common.cpp b/circuits/cpp/src/aztec3/circuits/kernel/private/common.cpp index 5e2fc9208de..7e49a3567c0 100644 --- a/circuits/cpp/src/aztec3/circuits/kernel/private/common.cpp +++ b/circuits/cpp/src/aztec3/circuits/kernel/private/common.cpp @@ -193,11 +193,10 @@ void common_update_end_values(DummyBuilder& builder, std::array siloed_nullified_commitments{}; for (size_t i = 0; i < MAX_NEW_NULLIFIERS_PER_CALL; ++i) { siloed_nullified_commitments[i] = - nullified_commitments[i] == fr(0) - ? fr(0) // don't silo when empty - : nullified_commitments[i] == fr(EMPTY_NULLIFIED_COMMITMENT) - ? fr(EMPTY_NULLIFIED_COMMITMENT) // don't silo when empty - : silo_commitment(storage_contract_address, nullified_commitments[i]); + nullified_commitments[i] == fr(0) ? fr(0) // don't silo when empty + : nullified_commitments[i] == fr(EMPTY_NULLIFIED_COMMITMENT) + ? fr(EMPTY_NULLIFIED_COMMITMENT) // don't silo when empty + : silo_commitment(storage_contract_address, nullified_commitments[i]); } push_array_to_array( diff --git a/docs/docs/dev_docs/dapps/testing.md b/docs/docs/dev_docs/dapps/testing.md new file mode 100644 index 00000000000..93fe000049e --- /dev/null +++ b/docs/docs/dev_docs/dapps/testing.md @@ -0,0 +1,178 @@ +# Testing + +Testing is an integral part of any piece of software, and especially important for any blockchain application. In this page we will cover how to interact with your Noir contracts in a testing environment to write automated tests for your apps. + +We will be using typescript to write our tests, and rely on the [`aztec.js`](https://www.npmjs.com/package/@aztec/aztec.js) client library to interact with a local Aztec network. We will use [`jest`](https://jestjs.io/) as a testing library, though feel free to use whatever you work with. Configuring the nodejs testing framework is out of scope for this guide. + +## A simple example + +Let's start with a simple example for a test using the [Sandbox](../sandbox/main.md). We will create two accounts and deploy a token contract in a setup step, and then issue a transfer from one user to another. + +#include_code sandbox-example /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +This test sets up the environment by creating a client to the Aztec RPC server running on the Sandbox on port 8080. It then creates two new accounts, dubbed `owner` and `recipient`. Last, it deploys an instance of the [`PrivateTokenContract`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/private_token_contract/src/main.nr), minting an initial 100 tokens to the owner. + +Once we have this setup, the test itself is simple. We check the balance of the `recipient` user to ensure it has no tokens, send and await a deployment transaction, and then check the balance again to ensure it was increased. Note that all numeric values are represented as [native bigints](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) to avoid loss of precision. + +:::info +We are using the `PrivateTokenContract` [typescript interface](../contracts/compiling.md#typescript-interfaces) to get type-safe methods for deploying and interacting with the token contract. +::: + +To run the test, first make sure the Sandbox is running on port 8080, and then [run your tests using jest](https://jestjs.io/docs/getting-started#running-from-command-line). Your test should pass, and you should see the following output in the Sandbox logs, where each chunk corresponds to a transaction. Note how this test run has a total of four transactions: two for deploying the account contracts for the `owner` and `recipient`, another for deploying the token contract, and a last one for actually executing the transfer. + +```text +rpc_server Registered account 0x2efa51d2e67581aef4578e8cc647a1af2e3f40e9872deeda0919e5f77cb8b2d2 +rpc_server Added contract SchnorrAccount at 0x2efa51d2e67581aef4578e8cc647a1af2e3f40e9872deeda0919e5f77cb8b2d2 +node Simulating tx 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +node Simulated tx 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 succeeds +rpc_server Executed local simulation for 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +rpc_server Sending transaction 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +node Received tx 19bfe4fb2569be2168f01eefe5e5a4284d6c1678f17ab5e94c6ba9c811bcb214 +sequencer Submitted rollup block 2 with 1 transactions + +rpc_server Registered account 0x12ef7ceb5064da3a729f598a6a50585059794fdcf347a6fc9bb317002162e3db +rpc_server Added contract SchnorrAccount at 0x12ef7ceb5064da3a729f598a6a50585059794fdcf347a6fc9bb317002162e3db +node Simulating tx 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +node Simulated tx 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 succeeds +rpc_server Executed local simulation for 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +rpc_server Sending transaction 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +node Received tx 0f195f8f6fb8fe29cf8159c5c664c1288788f1151a5413ec0e35cf378de74794 +sequencer Submitted rollup block 3 with 1 transactions + +rpc_server Added contract PrivateToken at 0x24e691d8bde970ab9e84fe669ea5ac8019c32c199a55aaa8a3e704db763af88f +node Simulating tx 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +node Simulated tx 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 succeeds +rpc_server Executed local simulation for 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +rpc_server Sending transaction 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +node Received tx 0101e1a3d73c3a112a18b7e4954edfe611d74ae0dc59e1688221ecda982ba943 +sequencer Submitted rollup block 4 with 1 transactions + +node Simulating tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +node Simulated tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c succeeds +rpc_server Executed local simulation for 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +rpc_server Sending transaction 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +node Received tx 2132767911fbbe67e24a3e51bc769ba2ae874eb1ba56e69cef8fc9e2c5eba04c +sequencer Submitted rollup block 5 with 1 transactions +``` + +### Using Sandbox initial accounts + +Instead of creating new accounts in our test suite, we can use the ones already initialized by the Sandbox upon startup. This can provide a speed boost to your tests setup. However, bear in mind that you may accidentally introduce an interdependency across test suites by reusing the same accounts. + +#include_code use-existing-wallets /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +### Running Sandbox in the nodejs process + +Instead of connecting to a local running Sandbox instance, you can also start your own Sandbox within the nodejs process running your tests, for an easier setup. To do this, import the `@aztec/aztec-sandbox` package in your project, and run `createSandbox` during setup. Note that this will still require you to run a local Ethereum node like Anvil locally. + +#include_code in-proc-sandbox /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +The `createSandbox` returns a `stop` callback that you should run once your test suite is over to stop all Sandbox services. + +#include_code stop-in-proc-sandbox /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +## Assertions + +We will now see how to use `aztec.js` to write assertions about transaction statuses, about chain state both public and private, and about logs. + +### Transactions + +In the example above we used `contract.methods.method().send().wait()` to create a function call for a contract, send it, and await it to be mined successfully. But what if we want to assert failing scenarios? + +#### 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. + +#include_code local-tx-fails /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +Under the hood, the `send()` method executes a simulation, so we can just call the usual `send().wait()` to catch the same failure. + +#include_code local-tx-fails-send /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### 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 a double-spend. 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. + +#include_code tx-dropped /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### 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 [`NativeTokenContract`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr) instead of a private one. + +:::info +Keep in mind that public function calls behave as in EVM blockchains, in that they are executed by the sequencer and not locally. Local simulation helps alert the user of a potential failure, but the actual execution path of a public function call will depend on when it gets mined. +::: + +#include_code local-pub-fails /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### A public call fails on the sequencer + +We can ignore a local simulation error for a public function via the `skipPublicSimulation`. This will submit a failing call to the sequencer, who will then reject it and drop the transaction. + +#include_code pub-dropped /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +If you run the snippet above, you'll see the following error in the Sandbox logs: + +``` +WARN Error processing tx 06dc87c4d64462916ea58426ffcfaf20017880b353c9ec3e0f0ee5fab3ea923f: Assertion failed: Balance too low. +``` + +:::info +In the near future, transactions where a public function call fails will get mined but flagged as reverted, instead of dropped. +::: + +### 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().view()`. Bear in mind that directly accessing contract storage will break any kind of encapsulation. + +To query storage directly, you'll need to know the slot you want to access. This can be checked in the [contract's `Storage` definition](../contracts/storage.md) directly for most data types. 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` utility class: + +#include_code calc-slot /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### Querying private state + +Private state in the Aztec Network is represented via sets of [private notes](../../concepts/foundation/state_model.md#private-state). In our token contract example, the balance of a user is represented as a set of unspent value notes, each with their own corresponding numeric value. + +#include_code value-note-def yarn-project/noir-libs/value-note/src/value_note.nr rust + +We can query the RPC server for all notes encrypted for a given user in a contract slot. For this example, we'll get all notes encrypted for the `owner` user that are stored on the token contract address and on the slot we calculated earlier. To calculate the actual balance, we extract the `value` of each note, which is the first element, and sum them up. + +#include_code private-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +#### Querying public state + +[Public state](../../concepts/foundation/state_model.md#public-state) behaves as a key-value store, much like in the EVM. This scenario is much more straightforward, in that we can directly query the target slot and get the result back as a buffer. Note that we use the [`NativeTokenContract`](https://github.com/AztecProtocol/aztec-packages/blob/master/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr) in this example, which defines a mapping of public balances on slot 4. + +#include_code public-storage /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +### Logs + +Last but not least, we can check the logs of [events](../contracts/events.md) emitted by our contracts. Contracts in Aztec can emit both [encrypted](../contracts/events.md#encrypted-events) and [unencrypted](../contracts/events.md#unencrypted-events) events. + +:::info +At the time of this writing, only unencrypted events can be queried directly. Encrypted events are always assumed to be encrypted notes. +::: + +#### Querying unencrypted logs + +We can query the RPC server for the unencrypted logs emitted in the block where our transaction is mined. Note that logs need to be unrolled and formatted as strings for consumption. + +#include_code unencrypted-logs /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +## Cheats + +The `CheatCodes` class, which we used for [calculating the storage slot above](#state), also includes a set of cheat methods for modifying the chain state that can be handy for testing. + +### Set next block timestamp + +The `warp` method sets the time for next execution, both on L1 and L2. We can test this using an `isTimeEqual` function in a `Test` contract defined like the following: + +#include_code is-time-equal yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr rust + +We can then call `warp` and rely on the `isTimeEqual` function to check that the timestamp was properly modified. + +#include_code warp /yarn-project/end-to-end/src/guides/dapp_testing.test.ts typescript + +:::info +The `warp` method calls `evm_setNextBlockTimestamp` under the hood on L1. +::: diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts index 509de59af8a..a5d3d296d5b 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_http/aztec_rpc_http_server.ts @@ -7,6 +7,7 @@ import { ContractData, ExtendedContractData, L2BlockL2Logs, + NotePreimage, PrivateKey, Tx, TxExecutionRequest, @@ -38,6 +39,7 @@ export function getHttpRpcServer(aztecRpcServer: AztecRPC): JsonRpcServer { Point, PrivateKey, Fr, + NotePreimage, }, { Tx, TxReceipt, L2BlockL2Logs }, false, diff --git a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts index 5a6034db556..18c3b732a3e 100644 --- a/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts +++ b/yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts @@ -177,6 +177,18 @@ export class AztecRPCServer implements AztecRPC { return await this.node.getPublicStorageAt(contract, storageSlot.value); } + public async getPrivateStorageAt(owner: AztecAddress, contract: AztecAddress, storageSlot: Fr) { + if ((await this.getContractData(contract)) === undefined) { + throw new Error(`Contract ${contract.toString()} is not deployed`); + } + const notes = await this.db.getNoteSpendingInfo(contract, storageSlot); + const ownerCompleteAddress = await this.db.getCompleteAddress(owner); + if (!ownerCompleteAddress) throw new Error(`Owner ${owner} not registered in RPC server`); + const { publicKey: ownerPublicKey } = ownerCompleteAddress; + const ownerNotes = notes.filter(n => n.publicKey.equals(ownerPublicKey)); + return ownerNotes.map(n => n.notePreimage); + } + public async getBlock(blockNumber: number): Promise { // If a negative block number is provided the current block number is fetched. if (blockNumber < 0) { diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json index 3bf0880a9a7..f565e533ef1 100644 --- a/yarn-project/aztec-sandbox/package.json +++ b/yarn-project/aztec-sandbox/package.json @@ -3,10 +3,9 @@ "version": "0.1.0", "type": "module", "exports": { - ".": "./dest/index.js", - "./http": "./dest/server.js" + ".": "./dest/index.js" }, - "bin": "./dest/index.js", + "bin": "./dest/bin/index.js", "typedocOptions": { "entryPoints": [ "./src/index.ts" @@ -17,7 +16,7 @@ "scripts": { "prepare": "node ../yarn-project-base/scripts/update_build_manifest.mjs package.json", "build": "yarn clean && tsc -b", - "start": "node --no-warnings ./dest", + "start": "node --no-warnings ./dest/bin", "clean": "rm -rf ./dest .tsbuildinfo", "formatting": "run -T prettier --check ./src && run -T eslint ./src", "formatting:fix": "run -T prettier -w ./src", diff --git a/yarn-project/aztec-sandbox/src/bin/index.ts b/yarn-project/aztec-sandbox/src/bin/index.ts new file mode 100644 index 00000000000..f9ac88c3964 --- /dev/null +++ b/yarn-project/aztec-sandbox/src/bin/index.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env -S node --no-warnings +import { deployInitialSandboxAccounts } from '@aztec/aztec.js'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { fileURLToPath } from '@aztec/foundation/url'; + +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; + +import { setupFileDebugLog } from '../logging.js'; +import { createSandbox } from '../sandbox.js'; +import { startHttpRpcServer } from '../server.js'; +import { github, splash } from '../splash.js'; + +const { SERVER_PORT = 8080 } = process.env; + +const logger = createDebugLogger('aztec:sandbox'); + +/** + * Create and start a new Aztec RCP HTTP Server + */ +async function main() { + const logPath = setupFileDebugLog(); + const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'); + const version = JSON.parse(readFileSync(packageJsonPath).toString()).version; + + logger.info(`Setting up Aztec Sandbox v${version}, please stand by...`); + + const { l1Contracts, rpcServer, stop } = await createSandbox(); + + logger.info('Setting up test accounts...'); + const accounts = await deployInitialSandboxAccounts(rpcServer); + + const shutdown = async () => { + logger.info('Shutting down...'); + await stop(); + process.exit(0); + }; + + process.once('SIGINT', shutdown); + process.once('SIGTERM', shutdown); + + startHttpRpcServer(rpcServer, l1Contracts, SERVER_PORT); + logger.info(`Aztec Sandbox JSON-RPC Server listening on port ${SERVER_PORT}`); + logger.info(`Debug logs will be written to ${logPath}`); + const accountStrings = [`Initial Accounts:\n\n`]; + + const registeredAccounts = await rpcServer.getAccounts(); + for (const account of accounts) { + const completeAddress = await account.account.getCompleteAddress(); + if (registeredAccounts.find(a => a.equals(completeAddress))) { + accountStrings.push(` Address: ${completeAddress.address.toString()}\n`); + accountStrings.push(` Partial Address: ${completeAddress.partialAddress.toString()}\n`); + accountStrings.push(` Private Key: ${account.privateKey.toString()}\n`); + accountStrings.push(` Public Key: ${completeAddress.publicKey.toString()}\n\n`); + } + } + logger.info(`${splash}\n${github}\n\n`.concat(...accountStrings).concat(`Aztec Sandbox is now ready for use!`)); +} + +main().catch(err => { + logger.error(err); + process.exit(1); +}); diff --git a/yarn-project/aztec-sandbox/src/index.ts b/yarn-project/aztec-sandbox/src/index.ts index 17b4da20df7..a9a6669f682 100644 --- a/yarn-project/aztec-sandbox/src/index.ts +++ b/yarn-project/aztec-sandbox/src/index.ts @@ -1,117 +1,2 @@ -#!/usr/bin/env -S node --no-warnings -import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; -import { createAztecRPCServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; -import { deployInitialSandboxAccounts } from '@aztec/aztec.js'; -import { PrivateKey } from '@aztec/circuits.js'; -import { deployL1Contracts } from '@aztec/ethereum'; -import { createDebugLogger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; -import { fileURLToPath } from '@aztec/foundation/url'; - -import { readFileSync } from 'fs'; -import { dirname, resolve } from 'path'; -import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem'; -import { mnemonicToAccount } from 'viem/accounts'; -import { foundry } from 'viem/chains'; - -import { setupFileDebugLog } from './logging.js'; -import { startHttpRpcServer } from './server.js'; -import { github, splash } from './splash.js'; - -const { SERVER_PORT = 8080, MNEMONIC = 'test test test test test test test test test test test junk' } = process.env; - -const logger = createDebugLogger('aztec:sandbox'); - -export const localAnvil = foundry; - -/** - * Helper function that waits for the Ethereum RPC server to respond before deploying L1 contracts. - */ -async function waitThenDeploy(rpcUrl: string, hdAccount: HDAccount) { - // wait for ETH RPC to respond to a request. - const publicClient = createPublicClient({ - chain: foundry, - transport: httpViemTransport(rpcUrl), - }); - const chainID = await retryUntil( - async () => { - let chainId = 0; - try { - chainId = await publicClient.getChainId(); - } catch (err) { - logger.warn(`Failed to connect to Ethereum node at ${rpcUrl}. Retrying...`); - } - return chainId; - }, - 'isEthRpcReady', - 600, - 1, - ); - - if (!chainID) { - throw Error(`Ethereum node unresponsive at ${rpcUrl}.`); - } - - // Deploy L1 contracts - const deployedL1Contracts = await deployL1Contracts(rpcUrl, hdAccount, localAnvil, logger); - return deployedL1Contracts; -} - -/** - * Create and start a new Aztec RCP HTTP Server - */ -async function main() { - const logPath = setupFileDebugLog(); - const aztecNodeConfig: AztecNodeConfig = getConfigEnvVars(); - const rpcConfig = getRpcConfigEnvVars(); - const hdAccount = mnemonicToAccount(MNEMONIC); - const privKey = hdAccount.getHdKey().privateKey; - const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); - const version = JSON.parse(readFileSync(packageJsonPath).toString()).version; - - logger.info(`Setting up Aztec Sandbox v${version}, please stand by...`); - logger.info('Deploying rollup contracts to L1...'); - const deployedL1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount); - aztecNodeConfig.publisherPrivateKey = new PrivateKey(Buffer.from(privKey!)); - aztecNodeConfig.rollupContract = deployedL1Contracts.rollupAddress; - aztecNodeConfig.contractDeploymentEmitterContract = deployedL1Contracts.contractDeploymentEmitterAddress; - aztecNodeConfig.inboxContract = deployedL1Contracts.inboxAddress; - - const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig); - const aztecRpcServer = await createAztecRPCServer(aztecNode, rpcConfig); - - logger.info('Setting up test accounts...'); - const accounts = await deployInitialSandboxAccounts(aztecRpcServer); - - const shutdown = async () => { - logger.info('Shutting down...'); - await aztecRpcServer.stop(); - await aztecNode.stop(); - process.exit(0); - }; - - process.once('SIGINT', shutdown); - process.once('SIGTERM', shutdown); - - startHttpRpcServer(aztecRpcServer, deployedL1Contracts, SERVER_PORT); - logger.info(`Aztec Sandbox JSON-RPC Server listening on port ${SERVER_PORT}`); - logger.info(`Debug logs will be written to ${logPath}`); - const accountStrings = [`Initial Accounts:\n\n`]; - - const registeredAccounts = await aztecRpcServer.getAccounts(); - for (const account of accounts) { - const completeAddress = await account.account.getCompleteAddress(); - if (registeredAccounts.find(a => a.equals(completeAddress))) { - accountStrings.push(` Address: ${completeAddress.address.toString()}\n`); - accountStrings.push(` Partial Address: ${completeAddress.partialAddress.toString()}\n`); - accountStrings.push(` Private Key: ${account.privateKey.toString()}\n`); - accountStrings.push(` Public Key: ${completeAddress.publicKey.toString()}\n\n`); - } - } - logger.info(`${splash}\n${github}\n\n`.concat(...accountStrings).concat(`Aztec Sandbox is now ready for use!`)); -} - -main().catch(err => { - logger.error(err); - process.exit(1); -}); +export { createSandbox } from './sandbox.js'; +export { startHttpRpcServer } from './server.js'; diff --git a/yarn-project/aztec-sandbox/src/sandbox.ts b/yarn-project/aztec-sandbox/src/sandbox.ts new file mode 100644 index 00000000000..ff1f7c7bc21 --- /dev/null +++ b/yarn-project/aztec-sandbox/src/sandbox.ts @@ -0,0 +1,84 @@ +#!/usr/bin/env -S node --no-warnings +import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; +import { createAztecRPCServer, getConfigEnvVars as getRpcConfigEnvVars } from '@aztec/aztec-rpc'; +import { PrivateKey } from '@aztec/circuits.js'; +import { deployL1Contracts } from '@aztec/ethereum'; +import { createDebugLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; + +import { HDAccount, createPublicClient, http as httpViemTransport } from 'viem'; +import { mnemonicToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +const { MNEMONIC = 'test test test test test test test test test test test junk' } = process.env; + +const logger = createDebugLogger('aztec:sandbox'); + +const localAnvil = foundry; + +/** + * Helper function that waits for the Ethereum RPC server to respond before deploying L1 contracts. + */ +async function waitThenDeploy(rpcUrl: string, hdAccount: HDAccount) { + // wait for ETH RPC to respond to a request. + const publicClient = createPublicClient({ + chain: foundry, + transport: httpViemTransport(rpcUrl), + }); + const chainID = await retryUntil( + async () => { + let chainId = 0; + try { + chainId = await publicClient.getChainId(); + } catch (err) { + logger.warn(`Failed to connect to Ethereum node at ${rpcUrl}. Retrying...`); + } + return chainId; + }, + 'isEthRpcReady', + 600, + 1, + ); + + if (!chainID) { + throw Error(`Ethereum node unresponsive at ${rpcUrl}.`); + } + + // Deploy L1 contracts + const deployedL1Contracts = await deployL1Contracts(rpcUrl, hdAccount, localAnvil, logger); + return deployedL1Contracts; +} + +/** Sandbox settings. */ +export type SandboxConfig = AztecNodeConfig & { + /** Mnemonic used to derive the L1 deployer private key.*/ + l1Mnemonic: string; +}; + +/** + * Create and start a new Aztec Node and RPC Server. Deploys L1 contracts. + * Does not start any HTTP services nor populate any initial accounts. + * @param config - Optional Sandbox settings. + */ +export async function createSandbox(config: Partial = {}) { + const aztecNodeConfig: AztecNodeConfig = { ...getConfigEnvVars(), ...config }; + const rpcConfig = getRpcConfigEnvVars(); + const hdAccount = mnemonicToAccount(config.l1Mnemonic ?? MNEMONIC); + const privKey = hdAccount.getHdKey().privateKey; + + const l1Contracts = await waitThenDeploy(aztecNodeConfig.rpcUrl, hdAccount); + aztecNodeConfig.publisherPrivateKey = new PrivateKey(Buffer.from(privKey!)); + aztecNodeConfig.rollupContract = l1Contracts.rollupAddress; + aztecNodeConfig.contractDeploymentEmitterContract = l1Contracts.contractDeploymentEmitterAddress; + aztecNodeConfig.inboxContract = l1Contracts.inboxAddress; + + const node = await AztecNodeService.createAndSync(aztecNodeConfig); + const rpcServer = await createAztecRPCServer(node, rpcConfig); + + const stop = async () => { + await rpcServer.stop(); + await node.stop(); + }; + + return { node, rpcServer, l1Contracts, stop }; +} diff --git a/yarn-project/aztec.js/package.json b/yarn-project/aztec.js/package.json index 4266ced7275..956b48c4183 100644 --- a/yarn-project/aztec.js/package.json +++ b/yarn-project/aztec.js/package.json @@ -43,6 +43,7 @@ "@aztec/types": "workspace:^", "lodash.every": "^4.6.0", "lodash.partition": "^4.6.0", + "lodash.zip": "^4.2.0", "tslib": "^2.4.0" }, "devDependencies": { @@ -51,6 +52,7 @@ "@types/jest": "^29.5.0", "@types/lodash.every": "^4.6.7", "@types/lodash.partition": "^4.6.0", + "@types/lodash.zip": "^4.2.7", "@types/node": "^18.7.23", "buffer": "^6.0.3", "crypto-browserify": "^3.12.0", diff --git a/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts b/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts index 3add6a538d5..075a14c9703 100644 --- a/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts +++ b/yarn-project/aztec.js/src/aztec_rpc_client/aztec_rpc_client.ts @@ -5,6 +5,7 @@ import { ContractData, ExtendedContractData, L2BlockL2Logs, + NotePreimage, Tx, TxExecutionRequest, TxHash, @@ -27,6 +28,7 @@ export const createAztecRpcClient = (url: string, fetch = defaultFetch): AztecRP Point, PrivateKey, Fr, + NotePreimage, }, { Tx, TxReceipt, L2BlockL2Logs }, false, diff --git a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts index 4734b0b0457..ee68855657f 100644 --- a/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts +++ b/yarn-project/aztec.js/src/aztec_rpc_client/wallet.ts @@ -7,6 +7,7 @@ import { FunctionCall, L2BlockL2Logs, NodeInfo, + NotePreimage, PackedArguments, SyncStatus, Tx, @@ -64,6 +65,9 @@ export abstract class BaseWallet implements Wallet { getTxReceipt(txHash: TxHash): Promise { return this.rpc.getTxReceipt(txHash); } + getPrivateStorageAt(owner: AztecAddress, contract: AztecAddress, storageSlot: Fr): Promise { + return this.rpc.getPrivateStorageAt(owner, contract, storageSlot); + } getPublicStorageAt(contract: AztecAddress, storageSlot: Fr): Promise { return this.rpc.getPublicStorageAt(contract, storageSlot); } @@ -173,6 +177,11 @@ export class AccountWallet extends EntrypointWallet { public getCompleteAddress() { return this.address; } + + /** Returns the address of the account that implements this wallet. */ + public getAddress() { + return this.address.address; + } } /** diff --git a/yarn-project/aztec.js/src/sandbox/index.ts b/yarn-project/aztec.js/src/sandbox/index.ts index 8c05e61937b..c0790b63960 100644 --- a/yarn-project/aztec.js/src/sandbox/index.ts +++ b/yarn-project/aztec.js/src/sandbox/index.ts @@ -2,8 +2,10 @@ import { Fr, PrivateKey } from '@aztec/circuits.js'; import { ContractAbi } from '@aztec/foundation/abi'; import { sleep } from '@aztec/foundation/sleep'; +import zip from 'lodash.zip'; + import SchnorrAccountContractAbi from '../abis/schnorr_account_contract.json' assert { type: 'json' }; -import { AztecRPC, getAccountWallets, getSchnorrAccount } from '../index.js'; +import { AccountWallet, AztecRPC, EntrypointWallet, getAccountWallets, getSchnorrAccount } from '../index.js'; export const INITIAL_SANDBOX_ENCRYPTION_KEYS = [ new PrivateKey(Buffer.from('b2803ec899f76f6b2ac011480d24028f1a29587f8a3a92f7ee9d48d8c085c284', 'hex')), @@ -18,11 +20,11 @@ export const INITIAL_SANDBOX_SALTS = [Fr.ZERO, Fr.ZERO, Fr.ZERO]; export const INITIAL_SANDBOX_ACCOUNT_CONTRACT_ABI = SchnorrAccountContractAbi; /** - * Gets a wallet for the Aztec accounts that are initially stored in the sandbox. + * Gets a single wallet that manages all the Aztec accounts that are initially stored in the sandbox. * @param aztecRpc - An instance of the Aztec RPC interface. - * @returns An AccountWallet implementation that includes all the accounts found. + * @returns An AccountWallet implementation that includes all the initial accounts. */ -export async function getSandboxAccountsWallet(aztecRpc: AztecRPC) { +export async function getSandboxAccountsWallet(aztecRpc: AztecRPC): Promise { return await getAccountWallets( aztecRpc, INITIAL_SANDBOX_ACCOUNT_CONTRACT_ABI as unknown as ContractAbi, @@ -32,6 +34,19 @@ export async function getSandboxAccountsWallet(aztecRpc: AztecRPC) { ); } +/** + * Gets a collection of wallets for the Aztec accounts that are initially stored in the sandbox. + * @param aztecRpc - An instance of the Aztec RPC interface. + * @returns A set of AccountWallet implementations for each of the initial accounts. + */ +export function getSandboxAccountsWallets(aztecRpc: AztecRPC): Promise { + return Promise.all( + zip(INITIAL_SANDBOX_ENCRYPTION_KEYS, INITIAL_SANDBOX_SIGNING_KEYS, INITIAL_SANDBOX_SALTS).map( + ([encryptionKey, signingKey, salt]) => getSchnorrAccount(aztecRpc, encryptionKey!, signingKey!, salt).getWallet(), + ), + ); +} + /** * Deploys the initial set of schnorr signature accounts to the sandbox * @param aztecRpc - An instance of the Aztec RPC interface. @@ -71,7 +86,7 @@ export async function deployInitialSandboxAccounts(aztecRpc: AztecRPC) { * Function to wait until the sandbox becomes ready for use. * @param rpcServer - The rpc client connected to the sandbox. */ -export const waitForSandbox = async (rpcServer: AztecRPC) => { +export async function waitForSandbox(rpcServer: AztecRPC) { while (true) { try { await rpcServer.getNodeInfo(); @@ -80,4 +95,4 @@ export const waitForSandbox = async (rpcServer: AztecRPC) => { await sleep(1000); } } -}; +} diff --git a/yarn-project/aztec.js/src/utils/cheat_codes.ts b/yarn-project/aztec.js/src/utils/cheat_codes.ts index d94b73b28e3..6321e0ff135 100644 --- a/yarn-project/aztec.js/src/utils/cheat_codes.ts +++ b/yarn-project/aztec.js/src/utils/cheat_codes.ts @@ -3,14 +3,10 @@ import { pedersenPlookupCommitInputs } from '@aztec/circuits.js/barretenberg'; import { toBigIntBE, toHex } from '@aztec/foundation/bigint-buffer'; import { keccak } from '@aztec/foundation/crypto'; import { createDebugLogger } from '@aztec/foundation/log'; -import { AztecRPC } from '@aztec/types'; +import { AztecRPC, NotePreimage } from '@aztec/types'; import fs from 'fs'; -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. */ @@ -236,13 +232,13 @@ export class AztecCheatCodes { * @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 { + public computeSlotInMap(baseSlot: Fr | bigint, key: Fr | bigint | AztecAddress): 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()), + [new Fr(baseSlot), new Fr(key)].map(f => f.toBuffer()), ), ); } @@ -277,10 +273,21 @@ export class AztecCheatCodes { * @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)); + const storageValue = await this.aztecRpc.getPublicStorageAt(who, new Fr(slot)); if (storageValue === undefined) { throw new Error(`Storage slot ${slot} not found`); } return Fr.fromBuffer(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 owner - The owner for whom the notes are encrypted + * @param slot - The storage slot to lookup + * @returns The notes stored at the given slot + */ + public loadPrivate(owner: AztecAddress, contract: AztecAddress, slot: Fr | bigint): Promise { + return this.aztecRpc.getPrivateStorageAt(owner, contract, new Fr(slot)); + } } diff --git a/yarn-project/end-to-end/src/e2e_cli.test.ts b/yarn-project/end-to-end/src/e2e_cli.test.ts index b18af474804..9f536f7b254 100644 --- a/yarn-project/end-to-end/src/e2e_cli.test.ts +++ b/yarn-project/end-to-end/src/e2e_cli.test.ts @@ -1,6 +1,6 @@ import { AztecNodeService } from '@aztec/aztec-node'; import { AztecAddress, AztecRPCServer } from '@aztec/aztec-rpc'; -import { startHttpRpcServer } from '@aztec/aztec-sandbox/http'; +import { startHttpRpcServer } from '@aztec/aztec-sandbox'; import { createDebugLogger } from '@aztec/aztec.js'; import { getProgram } from '@aztec/cli'; import { AztecRPC, CompleteAddress } from '@aztec/types'; diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts new file mode 100644 index 00000000000..e65a655da32 --- /dev/null +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -0,0 +1,219 @@ +import { createSandbox } from '@aztec/aztec-sandbox'; +import { + AccountWallet, + AztecRPC, + CheatCodes, + Fr, + L2BlockL2Logs, + PrivateKey, + createAztecRpcClient, + getSandboxAccountsWallets, + getSchnorrAccount, + waitForSandbox, +} from '@aztec/aztec.js'; +import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; +import { NativeTokenContract, PrivateTokenContract, TestContract } from '@aztec/noir-contracts/types'; + +describe('guides/dapp/testing', () => { + describe('on in-proc sandbox', () => { + describe('private token contract', () => { + let rpc: AztecRPC; + let stop: () => Promise; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + + beforeAll(async () => { + // docs:start:in-proc-sandbox + ({ rpcServer: rpc, stop } = await createSandbox()); + // docs:end:in-proc-sandbox + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + }, 30_000); + + // docs:start:stop-in-proc-sandbox + afterAll(() => stop()); + // docs:end:stop-in-proc-sandbox + + it('increases recipient funds on transfer', async () => { + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(0n); + await token.methods.transfer(20n, recipient.getAddress()).send().wait(); + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(20n); + }); + }); + }); + + describe('on local sandbox', () => { + beforeAll(async () => { + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + const rpc = createAztecRpcClient(SANDBOX_URL); + await waitForSandbox(rpc); + }); + + // docs:start:sandbox-example + describe('private token contract', () => { + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + + let rpc: AztecRPC; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + + beforeEach(async () => { + rpc = createAztecRpcClient(SANDBOX_URL); + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + }, 30_000); + + it('increases recipient funds on transfer', async () => { + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(0n); + await token.methods.transfer(20n, recipient.getAddress()).send().wait(); + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(20n); + }); + }); + // docs:end:sandbox-example + + describe('private token contract with initial accounts', () => { + const { SANDBOX_URL = 'http://localhost:8080' } = process.env; + + let rpc: AztecRPC; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + + beforeEach(async () => { + // docs:start:use-existing-wallets + rpc = createAztecRpcClient(SANDBOX_URL); + [owner, recipient] = await getSandboxAccountsWallets(rpc); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + // docs:end:use-existing-wallets + }, 30_000); + + it('increases recipient funds on transfer', async () => { + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(0n); + await token.methods.transfer(20n, recipient.getAddress()).send().wait(); + expect(await token.methods.getBalance(recipient.getAddress()).view()).toEqual(20n); + }); + }); + + describe('cheats', () => { + const { SANDBOX_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; + + let rpc: AztecRPC; + let owner: AccountWallet; + let testContract: TestContract; + let cheats: CheatCodes; + + beforeAll(async () => { + rpc = createAztecRpcClient(SANDBOX_URL); + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + testContract = await TestContract.deploy(owner).send().deployed(); + cheats = await CheatCodes.create(ETHEREUM_HOST, rpc); + }, 30_000); + + it('warps time to 1h into the future', async () => { + // docs:start:warp + const newTimestamp = Math.floor(Date.now() / 1000) + 60 * 60 * 24; + await cheats.aztec.warp(newTimestamp); + await testContract.methods.isTimeEqual(newTimestamp).send().wait(); + // docs:end:warp + }); + }); + + describe('assertions', () => { + const { SANDBOX_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost:8545' } = process.env; + + let rpc: AztecRPC; + let owner: AccountWallet; + let recipient: AccountWallet; + let token: PrivateTokenContract; + let nativeToken: NativeTokenContract; + let cheats: CheatCodes; + let ownerSlot: Fr; + + beforeAll(async () => { + rpc = createAztecRpcClient(SANDBOX_URL); + owner = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + recipient = await getSchnorrAccount(rpc, PrivateKey.random(), PrivateKey.random()).waitDeploy(); + token = await PrivateTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + nativeToken = await NativeTokenContract.deploy(owner, 100n, owner.getAddress()).send().deployed(); + + // docs:start:calc-slot + cheats = await CheatCodes.create(ETHEREUM_HOST, rpc); + // The balances mapping is defined on storage slot 1 and is indexed by user address + ownerSlot = cheats.aztec.computeSlotInMap(1n, owner.getAddress()); + // docs:end:calc-slot + }, 30_000); + + it('checks private storage', async () => { + // docs:start:private-storage + const notes = await rpc.getPrivateStorageAt(owner.getAddress(), token.address, ownerSlot); + const values = notes.map(note => note.items[0]); + const balance = values.reduce((sum, current) => sum + current.toBigInt(), 0n); + expect(balance).toEqual(100n); + // docs:end:private-storage + }); + + it('checks public storage', async () => { + // docs:start:public-storage + await nativeToken.methods.owner_mint_pub(owner.getAddress(), 100n).send().wait(); + const ownerPublicBalanceSlot = cheats.aztec.computeSlotInMap(4n, owner.getAddress()); + const balance = await rpc.getPublicStorageAt(nativeToken.address, ownerPublicBalanceSlot); + expect(toBigIntBE(balance!)).toEqual(100n); + // docs:end:public-storage + }); + + it('checks unencrypted logs', async () => { + // docs:start:unencrypted-logs + const tx = await nativeToken.methods.owner_mint_pub(owner.getAddress(), 100n).send().wait(); + const logs = await rpc.getUnencryptedLogs(tx.blockNumber!, 1); + const textLogs = L2BlockL2Logs.unrollLogs(logs).map(log => log.toString('ascii')); + expect(textLogs).toEqual(['Coins minted']); + // docs:end:unencrypted-logs + }); + + it('asserts a local transaction simulation fails by calling simulate', async () => { + // docs:start:local-tx-fails + const call = token.methods.transfer(200n, recipient.getAddress()); + await expect(call.simulate()).rejects.toThrowError(/Balance too low/); + // docs:end:local-tx-fails + }); + + it('asserts a local transaction simulation fails by calling send', async () => { + // docs:start:local-tx-fails-send + const call = token.methods.transfer(200n, recipient.getAddress()); + await expect(call.send().wait()).rejects.toThrowError(/Balance too low/); + // docs:end:local-tx-fails-send + }); + + it('asserts a transaction is dropped', async () => { + // docs:start:tx-dropped + const call1 = token.methods.transfer(80n, recipient.getAddress()); + const call2 = token.methods.transfer(50n, recipient.getAddress()); + + await call1.simulate(); + await call2.simulate(); + + await call1.send().wait(); + await expect(call2.send().wait()).rejects.toThrowError(/dropped/); + // docs:end:tx-dropped + }); + + it('asserts a simulation for a public function call fails', async () => { + // docs:start:local-pub-fails + const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 1000n); + await expect(call.simulate()).rejects.toThrowError(/Balance too low/); + // docs:end:local-pub-fails + }); + + it('asserts a transaction with a failing public call is dropped (until we get public reverts)', async () => { + // docs:start:pub-dropped + const call = nativeToken.methods.transfer_pub(recipient.getAddress(), 1000n); + await expect(call.send({ skipPublicSimulation: true }).wait()).rejects.toThrowError(/dropped/); + // docs:end:pub-dropped + }); + }); + }); +}); diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 284677ddac8..b0ecb881538 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -1,3 +1,4 @@ +import { AztecAddress } from '../aztec-address/index.js'; import { toBigIntBE, toBufferBE, toHex } from '../bigint-buffer/index.js'; import { randomBytes } from '../crypto/index.js'; import { BufferReader } from '../serialize/buffer_reader.js'; @@ -18,8 +19,8 @@ export class Fr { */ public readonly value: bigint; - constructor(value: bigint | number | Fr) { - const isFr = (value: bigint | number | Fr): value is Fr => !!(value as Fr).toBigInt; + constructor(value: bigint | number | Fr | AztecAddress) { + const isFr = (value: bigint | number | Fr | AztecAddress): value is Fr | AztecAddress => !!(value as Fr).toBigInt; this.value = isFr(value) ? value.toBigInt() : BigInt(value); if (this.value > Fr.MAX_VALUE) { throw new Error(`Fr out of range ${value}.`); diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index 003dae14cce..ba88a253519 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -182,10 +182,6 @@ ${abiStatement} export class ${input.name}Contract extends ContractBase { ${ctor} - get address() { - return this.completeAddress.address; - } - ${at} ${deploy} diff --git a/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr index 97f064e5ec8..27c8979b3fa 100644 --- a/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/native_token_contract/src/main.nr @@ -26,17 +26,19 @@ contract NativeToken { use crate::storage::Storage; - use dep::aztec::types::point::Point; use dep::aztec::{ note::{ note_header::NoteHeader, utils as note_utils, }, + oracle::{ + logs::emit_unencrypted_log, + compute_selector::compute_selector + }, + public_call_stack_item::PublicCallStackItem, + types::point::Point }; - use dep::aztec::public_call_stack_item::PublicCallStackItem; - use dep::aztec::oracle::compute_selector::compute_selector; - #[aztec(private)] fn constructor( initial_supply: Field, @@ -57,7 +59,8 @@ contract NativeToken { let new_balance = storage.public_balances.at(to).read() + amount; storage.public_balances.at(to).write(new_balance); storage.total_supply.write(storage.total_supply.read() + amount); - + let _hash = emit_unencrypted_log("Coins minted"); + 1 } @@ -190,7 +193,7 @@ contract NativeToken { let sender = context.msg_sender(); let sender_balance = storage.public_balances.at(sender); let current_sender_balance: Field = sender_balance.read(); - assert(current_sender_balance as u120 >= amount as u120); + assert(current_sender_balance as u120 >= amount as u120, "Balance too low"); let to_balance = storage.public_balances.at(to); let current_to_balance: Field = to_balance.read(); @@ -198,6 +201,8 @@ contract NativeToken { // User has sufficient balance so we decrement it by `amount` sender_balance.write(current_sender_balance - amount); to_balance.write(current_to_balance + amount); + + let _hash = emit_unencrypted_log("Coins transferred"); } #[aztec(public)] diff --git a/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr index 0c3573009af..41f398ead1c 100644 --- a/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/test_contract/src/main.nr @@ -105,14 +105,15 @@ contract Test { context.push_new_nullifier(note.get_commitment(), 0); } + // docs:start:is-time-equal #[aztec(public)] fn isTimeEqual( time: Field, ) -> Field { assert(context.timestamp() == time); - time } + // docs:end:is-time-equal // Purely exists for testing unconstrained fn getRandom( diff --git a/yarn-project/noir-libs/value-note/src/utils.nr b/yarn-project/noir-libs/value-note/src/utils.nr index 13e1d42db53..f431895e956 100644 --- a/yarn-project/noir-libs/value-note/src/utils.nr +++ b/yarn-project/noir-libs/value-note/src/utils.nr @@ -45,7 +45,7 @@ fn decrement( owner: Field, ) { let sum = decrement_by_at_most(balance, amount, owner); - assert(sum == amount); + assert(sum == amount, "Balance too low"); } // Similar to `decrement`, except that it doesn't fail if the decremented amount is less than max_amount. diff --git a/yarn-project/noir-libs/value-note/src/value_note.nr b/yarn-project/noir-libs/value-note/src/value_note.nr index 79f0213edb1..b4b79c1bceb 100644 --- a/yarn-project/noir-libs/value-note/src/value_note.nr +++ b/yarn-project/noir-libs/value-note/src/value_note.nr @@ -10,12 +10,14 @@ use dep::aztec::oracle::{ global VALUE_NOTE_LEN: Field = 3; // 3 plus a header. +// docs:start:value-note-def struct ValueNote { value: Field, owner: Field, randomness: Field, header: NoteHeader, } +// docs:end:value-note-def impl ValueNote { fn new(value: Field, owner: Field) -> Self { diff --git a/yarn-project/types/src/interfaces/aztec_rpc.ts b/yarn-project/types/src/interfaces/aztec_rpc.ts index b67ba94f11c..5e967e0cbb5 100644 --- a/yarn-project/types/src/interfaces/aztec_rpc.ts +++ b/yarn-project/types/src/interfaces/aztec_rpc.ts @@ -5,6 +5,7 @@ import { ContractData, ExtendedContractData, L2BlockL2Logs, + NotePreimage, Tx, TxExecutionRequest, TxHash, @@ -165,6 +166,17 @@ export interface AztecRPC { */ getTxReceipt(txHash: TxHash): Promise; + /** + * Retrieves the private storage data at a specified contract address and storage slot. + * The returned data is data at the storage slot or throws an error if the contract is not deployed. + * + * @param owner - The address for whom the private data is encrypted. + * @param contract - The AztecAddress of the target contract. + * @param storageSlot - The Fr representing the storage slot to be fetched. + * @returns A set of note preimages for the owner in that contract and slot. + */ + getPrivateStorageAt(owner: AztecAddress, contract: AztecAddress, storageSlot: Fr): Promise; + /** * Retrieves the public storage data at a specified contract address and storage slot. * The returned data is data at the storage slot or throws an error if the contract is not deployed. diff --git a/yarn-project/types/src/logs/note_spending_info/note_preimage.ts b/yarn-project/types/src/logs/note_spending_info/note_preimage.ts index a2a3c24126d..3d849147984 100644 --- a/yarn-project/types/src/logs/note_spending_info/note_preimage.ts +++ b/yarn-project/types/src/logs/note_spending_info/note_preimage.ts @@ -34,4 +34,22 @@ export class NotePreimage extends Vector { const items = Array.from({ length: numItems }, () => Fr.random()); return new NotePreimage(items); } + + /** + * Returns a hex representation of this preimage. + * @returns A hex string with the vector length as first element. + */ + toString() { + return '0x' + this.toBuffer().toString('hex'); + } + + /** + * Creates a new NotePreimage instance from a hex string. + * @param str - Hex representation. + * @returns A NotePreimage instance. + */ + static fromString(str: string) { + const hex = str.replace(/^0x/, ''); + return NotePreimage.fromBuffer(Buffer.from(hex, 'hex')); + } } diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index a63db18aeed..cd804f98754 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -198,7 +198,7 @@ __metadata: winston: ^3.10.0 winston-daily-rotate-file: ^4.7.1 bin: - aztec-sandbox: ./dest/index.js + aztec-sandbox: ./dest/bin/index.js languageName: unknown linkType: soft @@ -214,6 +214,7 @@ __metadata: "@types/jest": ^29.5.0 "@types/lodash.every": ^4.6.7 "@types/lodash.partition": ^4.6.0 + "@types/lodash.zip": ^4.2.7 "@types/node": ^18.7.23 buffer: ^6.0.3 crypto-browserify: ^3.12.0 @@ -221,6 +222,7 @@ __metadata: jest-mock-extended: ^3.0.3 lodash.every: ^4.6.0 lodash.partition: ^4.6.0 + lodash.zip: ^4.2.0 process: ^0.11.10 resolve-typescript-plugin: ^2.0.1 stream-browserify: ^3.0.0