diff --git a/aztec-up/bootstrap.sh b/aztec-up/bootstrap.sh index 27ecf742257b..50da97b1121d 100755 --- a/aztec-up/bootstrap.sh +++ b/aztec-up/bootstrap.sh @@ -95,7 +95,7 @@ EOF } function test_cmds { - for test in amm_flow bridge_and_claim basic_install counter_contract; do + for test in amm_flow bridge_and_claim basic_install counter_contract default_scaffold; do echo "$hash:TIMEOUT=15m aztec-up/scripts/run_test.sh $test" done } diff --git a/aztec-up/test/counter_contract.sh b/aztec-up/test/counter_contract.sh index 28f337ec3256..c4ce76350d56 100755 --- a/aztec-up/test/counter_contract.sh +++ b/aztec-up/test/counter_contract.sh @@ -5,27 +5,37 @@ export LOG_LEVEL=silent # Execute commands as per: https://docs.aztec.network/tutorials/codealong/contract_tutorials/counter_contract aztec new counter_contract -if [ ! -f counter_contract/Nargo.toml ] || [ ! -f counter_contract/src/main.nr ]; then - echo "Failed to create contract." + +# Verify workspace structure +if [ ! -f counter_contract/Nargo.toml ]; then + echo "Failed to create workspace Nargo.toml." + exit 1 +fi +if [ ! -f counter_contract/contract/Nargo.toml ] || [ ! -f counter_contract/contract/src/main.nr ]; then + echo "Failed to create contract crate." + exit 1 +fi +if [ ! -f counter_contract/test/Nargo.toml ] || [ ! -f counter_contract/test/src/lib.nr ]; then + echo "Failed to create test crate." exit 1 fi -# Check counter_contract dir is owned by aztec-dev. +# Check counter_contract dir is owned by ubuntu. if [ "$(stat -c %U counter_contract)" != "ubuntu" ]; then echo "counter_contract dir is not owned by ubuntu." exit 1 fi -# "Write" our contract. -cp -Rf ./aztec-packages/noir-projects/noir-contracts/contracts/test/counter_contract . +# "Write" our contract over the scaffold. +cp -Rf ./aztec-packages/noir-projects/noir-contracts/contracts/test/counter_contract/* counter_contract/ cd counter_contract -sed -i 's|\.\./\.\./\.\./\.\./|/home/ubuntu/aztec-packages/noir-projects/|g' Nargo.toml +sed -i 's|\.\./\.\./\.\./\.\./\.\./|/home/ubuntu/aztec-packages/noir-projects/|g' contract/Nargo.toml test/Nargo.toml # Compile the contract. aztec compile # Codegen -aztec codegen -o src/artifacts target -if [ ! -d src/artifacts ]; then +aztec codegen -o contract/src/artifacts target +if [ ! -d contract/src/artifacts ]; then echo "Failed to codegen TypeScript." exit 1 fi diff --git a/aztec-up/test/default_scaffold.sh b/aztec-up/test/default_scaffold.sh new file mode 100755 index 000000000000..d652bdbf8326 --- /dev/null +++ b/aztec-up/test/default_scaffold.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Tests that the default scaffold generated by `aztec new` compiles and passes its tests without any modifications. +# This catches regressions in the template files produced by setup_workspace.sh (e.g. syntax errors, stale imports, +# or API changes in aztec-nr) that would otherwise go unnoticed until a user runs `aztec new` themselves. + +export LOG_LEVEL=silent + +aztec new my_contract + +# Verify workspace structure. +if [ ! -f my_contract/Nargo.toml ]; then + echo "Failed to create workspace Nargo.toml." + exit 1 +fi +if [ ! -f my_contract/contract/Nargo.toml ] || [ ! -f my_contract/contract/src/main.nr ]; then + echo "Failed to create contract crate." + exit 1 +fi +if [ ! -f my_contract/test/Nargo.toml ] || [ ! -f my_contract/test/src/lib.nr ]; then + echo "Failed to create test crate." + exit 1 +fi + +cd my_contract + +# This is unfortunate as it makes the test worse but in CI setting the aztec version is 0.0.1 which doesn't exist as +# a remote git tag, so we need to rewrite dependencies to use local aztec-nr. +sed -i 's|aztec = .*git.*AztecProtocol/aztec-nr.*|aztec = { path="/home/ubuntu/aztec-packages/noir-projects/aztec-nr/aztec" }|' contract/Nargo.toml test/Nargo.toml + +# Compile the default scaffold contract. +aztec compile + +# Run the default scaffold tests. +aztec test diff --git a/boxes/init/.gitignore b/boxes/init/.gitignore new file mode 100644 index 000000000000..292dee672e8b --- /dev/null +++ b/boxes/init/.gitignore @@ -0,0 +1,2 @@ +target/ +codegenCache.json diff --git a/boxes/init/Nargo.toml b/boxes/init/Nargo.toml index 14bcaec85ac4..3ae352f74220 100644 --- a/boxes/init/Nargo.toml +++ b/boxes/init/Nargo.toml @@ -1,6 +1,2 @@ -[package] -name = "init" -type = "contract" - -[dependencies] -aztec = { path = "../../noir-projects/aztec-nr/aztec" } +[workspace] +members = ["contract", "test"] diff --git a/boxes/init/README.md b/boxes/init/README.md new file mode 100644 index 000000000000..5f0fe47a795f --- /dev/null +++ b/boxes/init/README.md @@ -0,0 +1,27 @@ +# init + +An Aztec Noir contract project. + +## Compile + +```bash +aztec compile +``` + +This compiles the contract in `contract/` and outputs artifacts to `target/`. + +## Test + +```bash +aztec test +``` + +This runs the tests in `test/`. + +## Generate TypeScript bindings + +```bash +aztec codegen target -o src/artifacts +``` + +This generates TypeScript contract artifacts from the compiled output in `target/` into `src/artifacts/`. diff --git a/boxes/init/contract/Nargo.toml b/boxes/init/contract/Nargo.toml new file mode 100644 index 000000000000..48e749065754 --- /dev/null +++ b/boxes/init/contract/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "init" +type = "contract" + +[dependencies] +aztec = { path = "../../../noir-projects/aztec-nr/aztec" } diff --git a/boxes/init/contract/src/main.nr b/boxes/init/contract/src/main.nr new file mode 100644 index 000000000000..fedcf9a88eea --- /dev/null +++ b/boxes/init/contract/src/main.nr @@ -0,0 +1,10 @@ +use aztec::macros::aztec; + +#[aztec] +pub contract Main { + use aztec::macros::functions::{external, initializer}; + + #[initializer] + #[external("private")] + fn constructor() {} +} diff --git a/boxes/init/src/main.nr b/boxes/init/src/main.nr deleted file mode 100644 index 302aec7a8469..000000000000 --- a/boxes/init/src/main.nr +++ /dev/null @@ -1,9 +0,0 @@ - -use aztec::macros::aztec; - -#[aztec] -contract Main { - #[external("private")] - #[initializer] - fn constructor() { } -} diff --git a/boxes/init/test/Nargo.toml b/boxes/init/test/Nargo.toml new file mode 100644 index 000000000000..38d2b87c645f --- /dev/null +++ b/boxes/init/test/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "init_test" +type = "lib" + +[dependencies] +aztec = { path = "../../../noir-projects/aztec-nr/aztec" } +init = { path = "../contract" } diff --git a/boxes/init/test/src/lib.nr b/boxes/init/test/src/lib.nr new file mode 100644 index 000000000000..7b5a395d4fd4 --- /dev/null +++ b/boxes/init/test/src/lib.nr @@ -0,0 +1,17 @@ +use aztec::test::helpers::test_environment::TestEnvironment; +use init::Main; + +#[test] +unconstrained fn test_constructor() { + let mut env = TestEnvironment::new(); + let deployer = env.create_light_account(); + + // Deploy the contract with the default constructor: + let contract_address = env.deploy("@init/Main").with_private_initializer( + deployer, + Main::interface().constructor(), + ); + + // Deploy without an initializer: + let contract_address = env.deploy("@init/Main").without_initializer(); +} diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md b/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md index f72f320384ed..61f30c88b3b6 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/contract_structure.md @@ -10,15 +10,23 @@ High-level structure of how Aztec smart contracts including the different compon ## Directory structure -Here's a common layout for a basic Aztec.nr Contract project: +When you create a new project with `aztec new`, it generates a workspace with two crates: a `contract` crate for your smart contract and a `test` crate for Noir tests. ```text title="layout of an aztec contract project" ─── my_aztec_contract_project - ├── src - │ └── main.nr <-- your contract - └── Nargo.toml <-- package and dependency management + ├── Nargo.toml <-- workspace root + ├── contract + │ ├── src + │ │ └── main.nr <-- your contract + │ └── Nargo.toml <-- contract package and dependencies + └── test + ├── src + │ └── lib.nr <-- your tests + └── Nargo.toml <-- test package and dependencies ``` +The workspace root `Nargo.toml` declares both crates as workspace members. The contract code lives in `contract/src/main.nr`, and tests live in a separate `test` crate that depends on the contract crate. + See the vanilla Noir docs for [more info on packages](https://noir-lang.org/docs/noir/modules_packages_crates/crates_and_packages). ## Contract block diff --git a/docs/docs-developers/docs/aztec-nr/index.md b/docs/docs-developers/docs/aztec-nr/index.md index 593e2630f136..34fe5705cc8f 100644 --- a/docs/docs-developers/docs/aztec-nr/index.md +++ b/docs/docs-developers/docs/aztec-nr/index.md @@ -42,16 +42,15 @@ storage.votes.insert(new_vote).deliver(vote_counter); // the vote counter accoun ### Flow -1. Write your contract and specify your contract dependencies. Every contract written for Aztec will have - aztec-nr as a dependency. Add it to your `Nargo.toml` with +1. Write your contract and specify your contract dependencies. Create a new project with `aztec new my_project`, which sets up a workspace with a `contract` crate and a `test` crate, with the `aztec` dependency already configured. If you need additional dependencies, add them to `contract/Nargo.toml`: ```toml -# Nargo.toml +# contract/Nargo.toml [dependencies] aztec = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_aztec_version", directory="aztec" } ``` -Update your `main.nr` contract file to use the Aztec.nr macros for writing contracts. +Update your `contract/src/main.nr` contract file to use the Aztec.nr macros for writing contracts. #include_code setup /docs/examples/contracts/counter_contract/src/main.nr rust diff --git a/docs/docs-developers/docs/aztec-nr/testing_contracts.md b/docs/docs-developers/docs/aztec-nr/testing_contracts.md index f53315dcf7c4..a0836ef628fa 100644 --- a/docs/docs-developers/docs/aztec-nr/testing_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/testing_contracts.md @@ -46,8 +46,10 @@ Always use `aztec test` instead of `nargo test`. The `TestEnvironment` requires ## Basic test structure +When you create a project with `aztec new` or `aztec init`, a separate `test` crate is created alongside the `contract` crate. Tests live in `test/src/lib.nr` and import the contract crate by name (not `crate::`): + ```rust -use crate::MyContract; +use my_contract::MyContract; use aztec::{ protocol::address::AztecAddress, test::helpers::test_environment::TestEnvironment, @@ -72,12 +74,11 @@ unconstrained fn test_basic_flow() { ::: :::tip Organizing test files -You can organize tests in separate files: +Tests live in the separate `test` crate that `aztec new` creates. You can organize them into modules: -- Create `src/test.nr` with `mod utils;` to import helper functions -- Split tests into modules like `src/test/transfer_tests.nr`, `src/test/auth_tests.nr` -- Import the test module in `src/main.nr` with `mod test;` -- Share setup functions in `src/test/utils.nr` +- Split tests into modules like `test/src/transfer_tests.nr`, `test/src/auth_tests.nr` +- Import them in `test/src/lib.nr` with `mod transfer_tests;`, `mod auth_tests;` +- Share setup functions in `test/src/utils.nr` ::: ## Deploying contracts diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 12a9b995b30e..196e8dffbc29 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,32 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### `aztec new` and `aztec init` now create a 2-crate workspace + +`aztec new` and `aztec init` now create a workspace with two crates instead of a single contract crate: + +- A `contract` crate (type = "contract") for your smart contract code +- A `test` crate (type = "lib") for Noir tests, which depends on the contract crate + +The new project structure looks like: + +``` +my_project/ +├── Nargo.toml # [workspace] members = ["contract", "test"] +├── contract/ +│ ├── src/main.nr +│ └── Nargo.toml # type = "contract" +└── test/ + ├── src/lib.nr + └── Nargo.toml # type = "lib" +``` + +**What changed:** +- The `--contract` and `--lib` flags have been removed from `aztec new` and `aztec init`. These commands now always create a contract workspace. +- Contract code is now at `contract/src/main.nr` instead of `src/main.nr`. +- The `Nargo.toml` in the project root is now a workspace file. Contract dependencies go in `contract/Nargo.toml`. +- Tests should be written in the separate `test` crate (`test/src/lib.nr`) and import the contract by package name (e.g., `use my_contract::MyContract;`) instead of using `crate::`. + ### Scope enforcement for private state access (TXE and PXE) Scope enforcement is now active across both TXE (test environment) and PXE (client). Previously, private execution could implicitly access any account's keys and notes. Now, only the caller (`from`) address is in scope by default, and accessing another address's private state requires explicitly granting scope. diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md b/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md index 0f8a7f672b99..817aa4f78273 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/counter_contract.md @@ -22,7 +22,7 @@ This tutorial is compatible with the Aztec version `#include_aztec_version`. Ins Run this to create a new contract project: ```bash -aztec new --contract counter +aztec new counter ``` Your structure should look like this: @@ -30,14 +30,20 @@ Your structure should look like this: ```tree . |-counter -| |-src -| | |-main.nr -| |-Nargo.toml +| |-Nargo.toml <-- workspace root +| |-contract +| | |-src +| | | |-main.nr +| | |-Nargo.toml <-- contract package config +| |-test +| | |-src +| | | |-lib.nr +| | |-Nargo.toml <-- test package config ``` -The file `main.nr` will soon turn into our smart contract! +The `aztec new` command creates a workspace with two crates: a `contract` crate for your smart contract code and a `test` crate for Noir tests. The file `contract/src/main.nr` will soon turn into our smart contract! -Add the following dependencies to `Nargo.toml` under the autogenerated content: +Add the following dependency to `contract/Nargo.toml` under the existing `aztec` dependency: ```toml [dependencies] @@ -47,7 +53,7 @@ balance_set = { git="https://github.com/AztecProtocol/aztec-nr/", tag="#include_ ## Define the functions -Go to `main.nr`, and replace the boilerplate code with this contract initialization: +Go to `contract/src/main.nr`, and replace the boilerplate code with this contract initialization: ```rust #include_code setup /docs/examples/contracts/counter_contract/src/main.nr raw diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md b/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md index 68b831dbecae..79eacfbfa4a9 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/recursive_verification.md @@ -210,24 +210,30 @@ The contract demonstrates several important patterns: ### Create the Contract Project -Use `aztec init` to generate the contract project structure: +Use `aztec new` to generate the contract project structure: ```bash -aztec init --contract contract +aztec new contract --name ValueNotEqual ``` -This creates: +This creates a workspace with two crates: ```tree contract/ -├── src/ -│ └── main.nr # Contract code -└── Nargo.toml # Contract configuration +├── Nargo.toml # Workspace root +├── contract/ +│ ├── src/ +│ │ └── main.nr # Contract code +│ └── Nargo.toml # Contract configuration +└── test/ + ├── src/ + │ └── lib.nr # Test code + └── Nargo.toml # Test configuration ``` ### Contract Configuration -Update `contract/Nargo.toml` with the required dependencies: +Update `contract/contract/Nargo.toml` with the required dependencies: ```toml [package] @@ -240,7 +246,7 @@ aztec = { git = "https://github.com/AztecProtocol/aztec-nr/", tag = "#include_az bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "#include_aztec_version", directory = "barretenberg/noir/bb_proof_verification" } ``` -**Key differences from the circuit's Nargo.toml**: +**Key differences from the circuit's Nargo.toml** (in `contract/contract/Nargo.toml`): - `type = "contract"` (not `"bin"`) - Depends on `aztec` for Aztec-specific features @@ -248,7 +254,7 @@ bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages ### Contract Structure -Replace the contents of `contract/src/main.nr` with: +Replace the contents of `contract/contract/src/main.nr` with: #include_code full_contract /docs/examples/contracts/recursive_verification_contract/src/main.nr rust @@ -380,7 +386,7 @@ Create the following files in your project root directory. "name": "recursive-verification-tutorial", "type": "module", "scripts": { - "ccc": "cd contract && aztec compile && aztec codegen target -o artifacts", + "ccc": "cd contract && aztec compile && aztec codegen target -o contract/artifacts", "data": "tsx scripts/generate_data.ts", "recursion": "tsx scripts/run_recursion.ts" }, @@ -451,7 +457,7 @@ yarn ccc This generates: - `contract/target/ValueNotEqual.json` - Contract artifact (bytecode, ABI, etc.) -- `contract/artifacts/ValueNotEqual.ts` - TypeScript class for deploying and interacting with the contract +- `contract/contract/artifacts/ValueNotEqual.ts` - TypeScript class for deploying and interacting with the contract ### Proof Generation Script @@ -584,7 +590,7 @@ import { SponsoredFeePaymentMethod } from "@aztec/aztec.js/fee"; import type { FieldLike } from "@aztec/aztec.js/abi"; import { getSponsoredFPCInstance } from "./sponsored_fpc.ts"; import { SponsoredFPCContract } from "@aztec/noir-contracts.js/SponsoredFPC"; -import { ValueNotEqualContract } from "../contract/artifacts/ValueNotEqual"; +import { ValueNotEqualContract } from "../contract/contract/artifacts/ValueNotEqual"; import data from "../data.json"; import { EmbeddedWallet } from "@aztec/wallets/embedded"; import { AztecAddress } from "@aztec/aztec.js/addresses"; diff --git a/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md b/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md index 6381094315ec..98e5fdc0fb60 100644 --- a/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md +++ b/docs/docs-developers/docs/tutorials/contract_tutorials/token_contract.md @@ -50,7 +50,7 @@ aztec init ## Contract structure -We have a messy, but working structure. In `src/main.nr` we even have a proto-contract. Let's replace it with a simple starting point: +The `aztec init` command created a workspace with two crates: a `contract` crate for your smart contract code and a `test` crate for Noir tests. In `contract/src/main.nr` we even have a proto-contract. Let's replace it with a simple starting point: ```rust #include_code start /docs/examples/contracts/bob_token_contract/src/main.nr raw @@ -60,7 +60,7 @@ We have a messy, but working structure. In `src/main.nr` we even have a proto-co The `#[aztec]` macro transforms our contract code to work with Aztec's privacy protocol. -Let's import the Aztec.nr library by adding it to our dependencies in `Nargo.toml`: +Let's make sure the Aztec.nr library is listed in our dependencies in `contract/Nargo.toml`: ```toml [package] @@ -254,7 +254,7 @@ In this case, all that the network sees (including Giggle) is just "something ha ### Updating Storage for Privacy -For something like balances, you can use a simple library called `easy_private_state` which abstracts away a custom private Note. A Note is at the core of how private state works in Aztec and you can read about it [here](../../foundational-topics/state_management.md). For now, let's just import the library in `Nargo.toml`: +For something like balances, you can use a simple library called `easy_private_state` which abstracts away a custom private Note. A Note is at the core of how private state works in Aztec and you can read about it [here](../../foundational-topics/state_management.md). For now, let's just import the library in `contract/Nargo.toml`: ```toml [dependencies] diff --git a/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md b/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md index 38a88723df02..f80d3c6897a5 100644 --- a/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md +++ b/docs/docs-developers/docs/tutorials/js_tutorials/token_bridge.md @@ -86,25 +86,20 @@ aztec new contracts/aztec/nft cd contracts/aztec/nft ``` +This creates a workspace with two crates: a `contract` crate for the smart contract code and a `test` crate for Noir tests. The `aztec` dependency is already configured in `contract/Nargo.toml`. + :::tip Noir Language Server If you're using VS Code, install the [Noir Language Support extension](https://marketplace.visualstudio.com/items?itemName=noir-lang.vscode-noir) for syntax highlighting, error checking, and code completion while writing Noir contracts. ::: -Open `Nargo.toml` and make sure `aztec` is a dependency: - -```toml -[dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-nr", tag = "#include_aztec_version", directory = "aztec" } -``` - ### Create the NFT Note -First, let's create a custom note type for private NFT ownership. In the `src/` directory, create a new file called `nft.nr`: +First, let's create a custom note type for private NFT ownership. In the `contract/src/` directory, create a new file called `nft.nr`: ```bash -touch src/nft.nr +touch contract/src/nft.nr ``` In this file, you're going to create a **private note** that represents NFT ownership. This is a struct with macros that indicate it is a note that can be compared and packed: @@ -121,7 +116,7 @@ Notes are powerful concepts. Learn more about how to use them in the [state mana ### Define Storage -Back in `main.nr`, you can now build the contract storage. You need: +Back in `contract/src/main.nr`, you can now build the contract storage. You need: - **admin**: Who controls the contract (set once, never changes) - **minter**: The bridge address (set once by admin) @@ -130,7 +125,7 @@ Back in `main.nr`, you can now build the contract storage. You need: One interesting aspect of this storage configuration is the use of `DelayedPublicMutable`, which allows private functions to read and use public state. You're using it to publicly track which NFTs are already minted while keeping their owners private. Read more about `DelayedPublicMutable` in [the storage guide](../../aztec-nr/framework-description/state_variables.md). -Write the storage struct and a simple [initializer](../../foundational-topics/contract_creation.md#initialization) to set the admin in the `main.nr` file: +Write the storage struct and a simple [initializer](../../foundational-topics/contract_creation.md#initialization) to set the admin in the `contract/src/main.nr` file: @@ -218,12 +213,12 @@ aztec new nft_bridge cd nft_bridge ``` -And again, add the `aztec-nr` dependency to `Nargo.toml`. We also need to add the `NFTPunk` contract we just wrote above: +Now add the `NFTPunk` contract dependency to `contract/Nargo.toml`. The `aztec` dependency is already there: ```toml [dependencies] aztec = { git="https://github.com/AztecProtocol/aztec-nr", tag = "#include_aztec_version", directory = "aztec" } -NFTPunk = { path = "../nft" } +NFTPunk = { path = "../../nft/contract" } ``` ### Understanding Bridges @@ -237,7 +232,7 @@ This means having knowledge about the L2 NFT contract, and the bridge on the L1 ### Bridge Storage -Clean up `main.nr` which is just a placeholder, and let's write the storage struct and the constructor. We'll use `PublicImmutable` since these values never change: +Clean up `contract/src/main.nr` which is just a placeholder, and let's write the storage struct and the constructor. We'll use `PublicImmutable` since these values never change: diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 6077b7dec019..9d35116f5a36 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -41,7 +41,7 @@ members = [ "contracts/test/avm_test_contract", "contracts/test/benchmarking_contract", "contracts/test/child_contract", - "contracts/test/counter_contract", + "contracts/test/counter_contract/contract", "contracts/test/event_only_contract", "contracts/test/import_test_contract", "contracts/test/invalid_account_contract", diff --git a/noir-projects/noir-contracts/bootstrap.sh b/noir-projects/noir-contracts/bootstrap.sh index e38c87d5ac44..74fd5ce1cc91 100755 --- a/noir-projects/noir-contracts/bootstrap.sh +++ b/noir-projects/noir-contracts/bootstrap.sh @@ -168,7 +168,7 @@ function compile { local contract_name contract_hash local contract_path=$(get_contract_path "$1" "$2") - local contract=${contract_path##*/} + local contract=$(grep -oP '(?<=^name = ")[^"]+' "$2/$contract_path/Nargo.toml") # Calculate filename because nargo... contract_name=$(cat $2/$contract_path/src/main.nr | awk '/^contract / { print $2 } /^pub contract / { print $3 }') local filename="$contract-$contract_name.json" diff --git a/noir-projects/noir-contracts/contracts/test/counter_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/counter_contract/Nargo.toml index 09b54cf10ec5..3ae352f74220 100644 --- a/noir-projects/noir-contracts/contracts/test/counter_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/test/counter_contract/Nargo.toml @@ -1,9 +1,2 @@ -[package] -name = "counter_contract" -authors = [""] -compiler_version = ">=0.25.0" -type = "contract" - -[dependencies] -aztec = { path = "../../../../aztec-nr/aztec" } -balance_set = { path = "../../../../aztec-nr/balance-set" } +[workspace] +members = ["contract", "test"] diff --git a/noir-projects/noir-contracts/contracts/test/counter_contract/contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/counter_contract/contract/Nargo.toml new file mode 100644 index 000000000000..9b2c58e42173 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/counter_contract/contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "counter_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../../aztec-nr/aztec" } +balance_set = { path = "../../../../../aztec-nr/balance-set" } diff --git a/noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/counter_contract/contract/src/main.nr similarity index 51% rename from noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr rename to noir-projects/noir-contracts/contracts/test/counter_contract/contract/src/main.nr index 6a735d3af490..87260f157ad1 100644 --- a/noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/counter_contract/contract/src/main.nr @@ -82,74 +82,4 @@ pub contract Counter { self.call(Counter::at(other_counter).increment(owner)); } - - mod test { - use crate::Counter; - use aztec::{ - protocol::address::AztecAddress, test::helpers::test_environment::TestEnvironment, - }; - - pub unconstrained fn setup( - initial_value: u128, - ) -> (TestEnvironment, AztecAddress, AztecAddress) { - // Setup env, generate keys - let mut env = TestEnvironment::new(); - let owner = env.create_light_account(); - - // Deploy contract and initialize - let initializer = Counter::interface().initialize(initial_value as u64, owner); - let contract_address = - env.deploy("Counter").with_private_initializer(owner, initializer); - (env, contract_address, owner) - } - - #[test] - unconstrained fn test_increment() { - let initial_value = 5; - let (mut env, contract_address, owner) = setup(initial_value); - - // Read the stored value in the note - let initial_counter = - env.execute_utility(Counter::at(contract_address).get_counter(owner)); - assert( - initial_counter == initial_value, - f"Expected {initial_value} but got {initial_counter}", - ); - - // Increment the counter - env.call_private(owner, Counter::at(contract_address).increment(owner)); - - let incremented_counter = - env.execute_utility(Counter::at(contract_address).get_counter(owner)); - let expected_current_value = initial_value + 1; - assert( - expected_current_value == incremented_counter, - f"Expected {expected_current_value} but got {incremented_counter}", - ); - } - - #[test] - unconstrained fn extended_incrementing_and_decrementing() { - let initial_value = 5; - let (env, contract_address, owner) = setup(initial_value); - - // Checking that the note was discovered from private logs - let initial_note_value = - env.execute_utility(Counter::at(contract_address).get_counter(owner)); - assert(initial_note_value == initial_value); - - env.call_private(owner, Counter::at(contract_address).increment_twice(owner)); - - assert_eq(env.execute_utility(Counter::at(contract_address).get_counter(owner)), 7); - - let _ = env.call_private( - owner, - Counter::at(contract_address).increment_and_decrement(owner), - ); - assert_eq(env.execute_utility(Counter::at(contract_address).get_counter(owner)), 7); - - env.call_private(owner, Counter::at(contract_address).decrement(owner)); - assert_eq(env.execute_utility(Counter::at(contract_address).get_counter(owner)), 6); - } - } } diff --git a/noir-projects/noir-contracts/contracts/test/counter_contract/src/test.nr b/noir-projects/noir-contracts/contracts/test/counter_contract/src/test.nr deleted file mode 100644 index 71f6bdde08e3..000000000000 --- a/noir-projects/noir-contracts/contracts/test/counter_contract/src/test.nr +++ /dev/null @@ -1,16 +0,0 @@ -use crate::Counter; -use aztec::{ - oracle::notes::set_sender_for_tags, protocol::address::AztecAddress, - test::helpers::test_environment::TestEnvironment, -}; - -pub unconstrained fn setup(initial_value: Field) -> (TestEnvironment, AztecAddress, AztecAddress) { - // Setup env, generate keys - let mut env = TestEnvironment::new(); - let owner = env.create_light_account(); - - // Deploy contract and initialize - let initializer = Counter::interface().initialize(initial_value as u64, owner); - let contract_address = env.deploy("Counter").with_private_initializer(owner, initializer); - (env, contract_address, owner) -} diff --git a/noir-projects/noir-contracts/contracts/test/counter_contract/test/Nargo.toml b/noir-projects/noir-contracts/contracts/test/counter_contract/test/Nargo.toml new file mode 100644 index 000000000000..1d5b103087e7 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/counter_contract/test/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "counter_contract_test" +authors = [""] +compiler_version = ">=0.25.0" +type = "lib" + +[dependencies] +aztec = { path = "../../../../../aztec-nr/aztec" } +balance_set = { path = "../../../../../aztec-nr/balance-set" } +counter_contract = { path = "../contract" } diff --git a/noir-projects/noir-contracts/contracts/test/counter_contract/test/src/lib.nr b/noir-projects/noir-contracts/contracts/test/counter_contract/test/src/lib.nr new file mode 100644 index 000000000000..2b1e84c12c3f --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/counter_contract/test/src/lib.nr @@ -0,0 +1,54 @@ +use aztec::{protocol::address::AztecAddress, test::helpers::test_environment::TestEnvironment}; +use counter_contract::Counter; + +pub unconstrained fn setup(initial_value: u128) -> (TestEnvironment, AztecAddress, AztecAddress) { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + let owner = env.create_light_account(); + + // Deploy contract and initialize + let initializer = Counter::interface().initialize(initial_value as u64, owner); + let contract_address = + env.deploy("@counter_contract/Counter").with_private_initializer(owner, initializer); + (env, contract_address, owner) +} + +#[test] +unconstrained fn test_increment() { + let initial_value = 5; + let (mut env, contract_address, owner) = setup(initial_value); + + // Read the stored value in the note + let initial_counter = env.execute_utility(Counter::at(contract_address).get_counter(owner)); + assert(initial_counter == initial_value, f"Expected {initial_value} but got {initial_counter}"); + + // Increment the counter + env.call_private(owner, Counter::at(contract_address).increment(owner)); + + let incremented_counter = env.execute_utility(Counter::at(contract_address).get_counter(owner)); + let expected_current_value = initial_value + 1; + assert( + expected_current_value == incremented_counter, + f"Expected {expected_current_value} but got {incremented_counter}", + ); +} + +#[test] +unconstrained fn extended_incrementing_and_decrementing() { + let initial_value = 5; + let (env, contract_address, owner) = setup(initial_value); + + // Checking that the note was discovered from private logs + let initial_note_value = env.execute_utility(Counter::at(contract_address).get_counter(owner)); + assert(initial_note_value == initial_value); + + env.call_private(owner, Counter::at(contract_address).increment_twice(owner)); + + assert_eq(env.execute_utility(Counter::at(contract_address).get_counter(owner)), 7); + + let _ = env.call_private(owner, Counter::at(contract_address).increment_and_decrement(owner)); + assert_eq(env.execute_utility(Counter::at(contract_address).get_counter(owner)), 7); + + env.call_private(owner, Counter::at(contract_address).decrement(owner)); + assert_eq(env.execute_utility(Counter::at(contract_address).get_counter(owner)), 6); +} diff --git a/playground/src/utils/networks.ts b/playground/src/utils/networks.ts index a221f5b6830a..5a02eb870e00 100644 --- a/playground/src/utils/networks.ts +++ b/playground/src/utils/networks.ts @@ -19,14 +19,14 @@ export type Network = { export const NETWORKS: Network[] = [ { - nodeURL: 'https://next.devnet.aztec-labs.com', + nodeURL: 'https://v4-devnet-2.aztec-labs.com/', name: 'Aztec Devnet', description: 'Public development network', chainId: 11155111, - version: 1647720761, + version: 615022430, hasTestAccounts: false, hasSponsoredFPC: true, - nodeVersion: '3.0.0-devnet', + nodeVersion: '4.0.0-devnet.2-patch.1', }, { nodeURL: 'http://localhost:8080', diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 88bb1404b59e..96b30ee28dfa 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -70,7 +70,8 @@ import { type WorldStateSynchronizer, tryStop, } from '@aztec/stdlib/interfaces/server'; -import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; +import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; +import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs'; import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging'; import { P2PClientType } from '@aztec/stdlib/p2p'; import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing'; @@ -151,12 +152,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { private blobClient?: BlobClientInterface, private validatorClient?: ValidatorClient, private keyStoreManager?: KeystoreManager, + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.metrics = new NodeMetrics(telemetry, 'AztecNodeService'); this.tracer = telemetry.getTracer('AztecNodeService'); this.log.info(`Aztec Node version: ${this.packageVersion}`); this.log.info(`Aztec Node started on chain 0x${l1ChainId.toString(16)}`, config.l1Contracts); + + // A defensive check that protects us against introducing a bug in the complex `createAndSync` function. We must + // never have debugLogStore enabled when not in test mode because then we would be accumulating debug logs in + // memory which could be a DoS vector on the sequencer (since no fees are paid for debug logs). + if (debugLogStore.isEnabled && config.realProofs) { + throw new Error('debugLogStore should never be enabled when realProofs are set'); + } } public async getWorldStateSyncStatus(): Promise { @@ -296,9 +305,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { config.realProofs || config.debugForceTxProofVerification ? await BBCircuitVerifier.new(config) : new TestCircuitVerifier(config.proverTestVerificationDelayMs); + + let debugLogStore: DebugLogStore; if (!config.realProofs) { log.warn(`Aztec node is accepting fake proofs`); + + debugLogStore = new InMemoryDebugLogStore(); + log.info( + 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served', + ); + } else { + debugLogStore = new NullDebugLogStore(); } + const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier); // create the tx pool and the p2p client, which will need the l2 block source @@ -457,6 +476,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { archiver, dateProvider, telemetry, + debugLogStore, ); sequencer = await SequencerClient.new(config, { @@ -538,6 +558,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { blobClient, validatorClient, keyStoreManager, + debugLogStore, ); return node; @@ -831,18 +852,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { // Then get the actual tx from the archiver, which tracks every tx in a mined block. const settledTxReceipt = await this.blockSource.getSettledTxReceipt(txHash); + let receipt: TxReceipt; if (settledTxReceipt) { - // If the archiver has the receipt then return it. - return settledTxReceipt; + receipt = settledTxReceipt; } else if (isKnownToPool) { // If the tx is in the pool but not in the archiver, it's pending. // This handles race conditions between archiver and p2p, where the archiver // has pruned the block in which a tx was mined, but p2p has not caught up yet. - return new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined); + receipt = new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined); } else { // Otherwise, if we don't know the tx, we consider it dropped. - return new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node'); + receipt = new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node'); } + + this.debugLogStore.decorateReceiptWithLogs(txHash.toString(), receipt); + + return receipt; } public getTxEffect(txHash: TxHash): Promise { diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 3ff7ebc9914a..c91e54b34dbd 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -9,7 +9,14 @@ import { getContractInstanceFromInstantiationParams, } from '@aztec/stdlib/contract'; import type { PublicKeys } from '@aztec/stdlib/keys'; -import { type Capsule, TxHash, type TxProfileResult, type TxReceipt, collectOffchainEffects } from '@aztec/stdlib/tx'; +import { + type Capsule, + HashedValues, + TxHash, + type TxProfileResult, + type TxReceipt, + collectOffchainEffects, +} from '@aztec/stdlib/tx'; import { ExecutionPayload, mergeExecutionPayloads } from '@aztec/stdlib/tx'; import { publishContractClass } from '../deployment/publish_class.js'; @@ -164,6 +171,7 @@ export class DeployMethod extends constructorNameOrArtifact?: string | FunctionArtifact, authWitnesses: AuthWitness[] = [], capsules: Capsule[] = [], + private extraHashedArgs: HashedValues[] = [], ) { super(wallet, authWitnesses, capsules); this.constructorArtifact = getInitializer(artifact, constructorNameOrArtifact); @@ -174,20 +182,29 @@ export class DeployMethod extends * @param options - Configuration options. * @returns The execution payload for this operation */ - public async request(options?: RequestDeployOptions): Promise { + public async request(options: RequestDeployOptions = {}): Promise { const publication = await this.getPublicationExecutionPayload(options); if (!options?.skipRegistration) { await this.wallet.registerContract(await this.getInstance(options), this.artifact); } - + const { authWitnesses, capsules } = options; + + // Propagates the included authwitnesses, capsules, and extraHashedArgs + // potentially baked into the interaction + const initialExecutionPayload = new ExecutionPayload( + [], + this.authWitnesses.concat(authWitnesses ?? []), + this.capsules.concat(capsules ?? []), + this.extraHashedArgs, + ); const initialization = await this.getInitializationExecutionPayload(options); const feeExecutionPayload = options?.fee?.paymentMethod ? await options.fee.paymentMethod.getExecutionPayload() : undefined; const finalExecutionPayload = feeExecutionPayload - ? mergeExecutionPayloads([feeExecutionPayload, publication, initialization]) - : mergeExecutionPayloads([publication, initialization]); + ? mergeExecutionPayloads([initialExecutionPayload, feeExecutionPayload, publication, initialization]) + : mergeExecutionPayloads([initialExecutionPayload, publication, initialization]); if (!finalExecutionPayload.calls.length) { throw new Error(`No transactions are needed to publish or initialize contract ${this.artifact.name}`); } diff --git a/yarn-project/aztec/bootstrap.sh b/yarn-project/aztec/bootstrap.sh index 337b7e40d9c3..c27fba277781 100755 --- a/yarn-project/aztec/bootstrap.sh +++ b/yarn-project/aztec/bootstrap.sh @@ -4,11 +4,14 @@ source $(git rev-parse --show-toplevel)/ci3/source_bootstrap repo_root=$(git rev-parse --show-toplevel) export NARGO=${NARGO:-$repo_root/noir/noir-repo/target/release/nargo} export BB=${BB:-$repo_root/barretenberg/cpp/build/bin/bb} +export PROFILER_PATH=${PROFILER_PATH:-$repo_root/noir/noir-repo/target/release/noir-profiler} hash=$(../bootstrap.sh hash) function test_cmds { - echo "$hash:ISOLATE=1:NAME=aztec/src/cli/cmds/compile.test.ts NARGO=$NARGO BB=$BB yarn-project/scripts/run_test.sh aztec/src/cli/cmds/compile.test.ts" + # All CLI tests share test/mixed-workspace/target so they must run sequentially + # in a single jest invocation (--runInBand is set by run_test.sh). + echo "$hash:ISOLATE=1:NAME=aztec/cli NARGO=$NARGO BB=$BB PROFILER_PATH=$PROFILER_PATH yarn-project/scripts/run_test.sh aztec/src/cli" } case "$cmd" in diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index bf73218d3949..e5573d79c880 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -53,9 +53,13 @@ case $cmd in aztec start "$@" ;; - new|init|flamegraph) + new|init) $script_dir/${cmd}.sh "$@" ;; + flamegraph) + echo "Warning: 'aztec flamegraph' is deprecated. Use 'aztec profile flamegraph' instead." >&2 + aztec profile flamegraph "$@" + ;; *) aztec $cmd "$@" ;; diff --git a/yarn-project/aztec/scripts/extract_function.js b/yarn-project/aztec/scripts/extract_function.js deleted file mode 100644 index c73c8ba9aa58..000000000000 --- a/yarn-project/aztec/scripts/extract_function.js +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node -import fs from 'fs/promises'; -import path from 'path'; - -// Simple script to extract a contract function as a separate Noir artifact. -// We need to use this since the transpiling that we do on public functions make the contract artifacts -// unreadable by noir tooling, since they are no longer following the noir artifact format. -async function main() { - let [contractArtifactPath, functionName] = process.argv.slice(2); - if (!contractArtifactPath || !functionName) { - console.log('Usage: node extractFunctionAsNoirArtifact.js '); - return; - } - - const contractArtifact = JSON.parse(await fs.readFile(contractArtifactPath, 'utf8')); - const func = contractArtifact.functions.find(f => f.name === functionName); - if (!func) { - console.error(`Function ${functionName} not found in ${contractArtifactPath}`); - return; - } - - const artifact = { - noir_version: contractArtifact.noir_version, - hash: 0, - abi: func.abi, - bytecode: func.bytecode, - debug_symbols: func.debug_symbols, - file_map: contractArtifact.file_map, - expression_width: { - Bounded: { - width: 4, - }, - }, - }; - - const outputDir = path.dirname(contractArtifactPath); - const outputName = path.basename(contractArtifactPath, '.json') + `-${functionName}.json`; - - const outPath = path.join(outputDir, outputName); - - await fs.writeFile(outPath, JSON.stringify(artifact, null, 2)); -} - -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/yarn-project/aztec/scripts/flamegraph.sh b/yarn-project/aztec/scripts/flamegraph.sh deleted file mode 100755 index 48763ef0d793..000000000000 --- a/yarn-project/aztec/scripts/flamegraph.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -set -eu - -# If first arg is -h or --help, print usage. -if [ $# -lt 2 ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]; then - cat << 'EOF' -Aztec Flamegraph - Generate a gate count flamegraph for an aztec contract function. - -Usage: aztec flamegraph - -Options: - -h, --help Print help - -Will output an svg at /--flamegraph.svg. -You can open it in your browser to view it. - -EOF - exit 0 -fi - -cleanup() { - set +e - if [ -f "$function_artifact" ]; then - rm -f "$function_artifact" - fi -} - -trap cleanup EXIT - -# Get the directory of the script -script_dir=$(realpath $(dirname $0)) - -PROFILER=${PROFILER_PATH:-noir-profiler} -BB=${BB:-bb} - -# first console arg is contract name in camel case or path to contract artifact -contract=$1 - -# second console arg is the contract function -function=$2 - -if [ ! -f "$contract" ]; then - echo "Error: Contract artifact not found at: $contract" - exit 1 -fi -artifact_path=$contract -function_artifact="${artifact_path%%.json}-${function}.json" -output_dir=$(dirname "$artifact_path") - -# Extract artifact for the specific function. -node $script_dir/extract_function.js "$artifact_path" $function - -# Generate the flamegraph -$PROFILER gates --artifact-path "$function_artifact" --backend-path "$BB" --backend-gates-command "gates" --output "$output_dir" --scheme chonk --include_gates_per_opcode - -# Save as $artifact_name-$function-flamegraph.svg -output_file="${function_artifact%%.json}-flamegraph.svg" -mv "$output_dir/__aztec_nr_internals__${function}_gates.svg" "$output_file" -echo "Flamegraph generated at: $output_file" diff --git a/yarn-project/aztec/scripts/init.sh b/yarn-project/aztec/scripts/init.sh index fa66617ff6ed..3fae0e67f894 100755 --- a/yarn-project/aztec/scripts/init.sh +++ b/yarn-project/aztec/scripts/init.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -NARGO=${NARGO:-nargo} script_path=$(realpath $(dirname "$0")) +name_arg="" + +# Check for help first for arg in "$@"; do if [ "$arg" == "--help" ] || [ "$arg" == "-h" ]; then cat << 'EOF' @@ -13,23 +15,31 @@ Usage: aztec init [OPTIONS] Options: --name Name of the package [default: current directory name] - --lib Use a library template -h, --help Print help -This command creates a new Aztec Noir project in the current directory using nargo -and automatically adds the Aztec.nr dependency to your Nargo.toml file. - +This command creates a new Aztec Noir project in the current directory with +a workspace containing a contract crate and a test crate, and automatically +adds the Aztec.nr dependency to both. EOF exit 0 fi - if [ "$arg" == "--lib" ]; then - is_contract=0 - fi done -echo "Initializing Noir project..." -$NARGO init "$@" +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --name) + name_arg="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +# Derive package name: use --name if provided, otherwise use current directory name +package_name="${name_arg:-$(basename $(pwd))}" -if [ "${is_contract:-1}" -eq 1 ]; then - $script_path/setup_project.sh -fi +echo "Initializing Aztec contract project..." +$script_path/setup_workspace.sh "$package_name" diff --git a/yarn-project/aztec/scripts/new.sh b/yarn-project/aztec/scripts/new.sh index 6901a235bfe8..37584fffae0a 100755 --- a/yarn-project/aztec/scripts/new.sh +++ b/yarn-project/aztec/scripts/new.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -NARGO=${NARGO:-nargo} script_path=$(realpath $(dirname "$0")) -type_arg="--contract" +name_arg="" +project_path="" while [[ $# -gt 0 ]]; do case $1 in @@ -19,20 +19,16 @@ Arguments: Options: --name Name of the package [default: package directory name] - --lib Create a library template instead of a contract -h, --help Print help -This command creates a new Aztec Noir project using nargo and automatically -adds the Aztec.nr dependency to your Nargo.toml file. +This command creates a new Aztec Noir project with a workspace containing +a contract crate and a test crate, and automatically adds the Aztec.nr +dependency to both. EOF exit 0 ;; - --lib) - type_arg="--lib" - shift - ;; --name) - name_arg="--name $2" + name_arg="$2" shift 2 ;; *) @@ -50,10 +46,15 @@ if [ -z "$project_path" ]; then exit 1 fi -echo "Creating new Noir project at $project_path..." -$NARGO new $type_arg ${name_arg:-} $project_path - -if [ "$type_arg" == "--contract" ]; then - cd $project_path - $script_path/setup_project.sh +if [ -d "$project_path" ] && [ "$(ls -A $project_path 2>/dev/null)" ]; then + echo "Error: $project_path already exists and is not empty" + exit 1 fi + +# Derive package name: use --name if provided, otherwise use directory basename +package_name="${name_arg:-$(basename $project_path)}" + +echo "Creating new Aztec contract project at $project_path..." +mkdir -p "$project_path" +cd "$project_path" +$script_path/setup_workspace.sh "$package_name" diff --git a/yarn-project/aztec/scripts/setup_project.sh b/yarn-project/aztec/scripts/setup_project.sh deleted file mode 100755 index 1797a5625491..000000000000 --- a/yarn-project/aztec/scripts/setup_project.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Get the actual aztec version for the git tag. -AZTEC_VERSION=$(jq -r '.version' $(dirname $0)/../package.json) -NARGO_TOML_PATH="Nargo.toml" -MAIN_NR_PATH="src/main.nr" - -if [ ! -f "$NARGO_TOML_PATH" ]; then - >&2 echo "Warning: Could not find Nargo.toml at $NARGO_TOML_PATH to add aztec dependency" - exit 1 -fi - -if [ ! -f "$MAIN_NR_PATH" ]; then - >&2 echo "Warning: Could not find main.nr at $MAIN_NR_PATH" - exit 1 -fi - -# Add aztec dependency to Nargo.toml -echo "" >> "$NARGO_TOML_PATH" -echo "aztec = { git=\"https://github.com/AztecProtocol/aztec-nr\", tag=\"v${AZTEC_VERSION}\", directory=\"aztec\" }" >> "$NARGO_TOML_PATH" -echo "Added aztec dependency (v${AZTEC_VERSION}) to Nargo.toml" - -# Replace the contents of main.nr with the Aztec contract template -cat > "$MAIN_NR_PATH" << 'EOF' -use aztec::macros::aztec; - -#[aztec] -contract Main {} -EOF -echo "Created main.nr with Aztec contract template" diff --git a/yarn-project/aztec/scripts/setup_workspace.sh b/yarn-project/aztec/scripts/setup_workspace.sh new file mode 100755 index 000000000000..11ca9deb68f0 --- /dev/null +++ b/yarn-project/aztec/scripts/setup_workspace.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Creates an Aztec contract workspace with a contract crate and a test crate. +# Usage: setup_workspace.sh +# Must be called from the workspace root directory. + +package_name=$1 + +if [ -z "$package_name" ]; then + echo "Error: package name is required" + exit 1 +fi + +if [ -f "Nargo.toml" ]; then + echo "Error: Nargo.toml already exists in the current directory" + exit 1 +fi + +# Get the actual aztec version for the git tag. +AZTEC_VERSION=$(jq -r '.version' $(dirname $0)/../package.json) + +# Create workspace root Nargo.toml +cat > Nargo.toml << 'EOF' +[workspace] +members = ["contract", "test"] +EOF + +# Create contract crate +mkdir -p contract/src +cat > contract/Nargo.toml << CEOF +[package] +name = "${package_name}" +type = "contract" + +[dependencies] +aztec = { git="https://github.com/AztecProtocol/aztec-nr", tag="v${AZTEC_VERSION}", directory="aztec" } +CEOF + +cat > contract/src/main.nr << 'EOF' +use aztec::macros::aztec; + +#[aztec] +pub contract Main { + use aztec::macros::functions::{external, initializer}; + + #[initializer] + #[external("private")] + fn constructor() {} +} +EOF + +# Create test crate +mkdir -p test/src +cat > test/Nargo.toml << TEOF +[package] +name = "${package_name}_test" +type = "lib" + +[dependencies] +aztec = { git="https://github.com/AztecProtocol/aztec-nr", tag="v${AZTEC_VERSION}", directory="aztec" } +${package_name} = { path = "../contract" } +TEOF + +cat > test/src/lib.nr << 'NOIR' +use aztec::test::helpers::test_environment::TestEnvironment; +use __PACKAGE_NAME__::Main; + +#[test] +unconstrained fn test_constructor() { + let mut env = TestEnvironment::new(); + let deployer = env.create_light_account(); + + // Deploy the contract with the default constructor: + let contract_address = env.deploy("@__PACKAGE_NAME__/Main").with_private_initializer( + deployer, + Main::interface().constructor(), + ); + + // Deploy without an initializer: + let contract_address = env.deploy("@__PACKAGE_NAME__/Main").without_initializer(); +} +NOIR + +sed -i "s/__PACKAGE_NAME__/${package_name}/g" test/src/lib.nr + +# Create README +cat > README.md << REOF +# ${package_name} + +An Aztec Noir contract project. + +## Compile + +\`\`\`bash +aztec compile +\`\`\` + +This compiles the contract in \`contract/\` and outputs artifacts to \`target/\`. + +## Test + +\`\`\`bash +aztec test +\`\`\` + +This runs the tests in \`test/\`. + +## Generate TypeScript bindings + +\`\`\`bash +aztec codegen target -o src/artifacts +\`\`\` + +This generates TypeScript contract artifacts from the compiled output in \`target/\` into \`src/artifacts/\`. +REOF + +# Create .gitignore +cat > .gitignore << 'GEOF' +target/ +codegenCache.json +GEOF + +echo "Created Aztec contract workspace with crates '${package_name}' and '${package_name}_test'" diff --git a/yarn-project/aztec/src/bin/index.ts b/yarn-project/aztec/src/bin/index.ts index 90a915778b17..c1565d92576f 100644 --- a/yarn-project/aztec/src/bin/index.ts +++ b/yarn-project/aztec/src/bin/index.ts @@ -16,6 +16,7 @@ import { Command } from 'commander'; import { injectCompileCommand } from '../cli/cmds/compile.js'; import { injectMigrateCommand } from '../cli/cmds/migrate_ha_db.js'; +import { injectProfileCommand } from '../cli/cmds/profile.js'; import { injectAztecCommands } from '../cli/index.js'; import { getCliVersion } from '../cli/release_version.js'; @@ -58,6 +59,7 @@ async function main() { program = injectMiscCommands(program, userLog); program = injectValidatorKeysCommands(program, userLog); program = injectCompileCommand(program, userLog); + program = injectProfileCommand(program, userLog); program = injectMigrateCommand(program, userLog); await program.parseAsync(process.argv); diff --git a/yarn-project/aztec/src/cli/cmds/compile.ts b/yarn-project/aztec/src/cli/cmds/compile.ts index 9fb8aba37f03..9737eeb9b312 100644 --- a/yarn-project/aztec/src/cli/cmds/compile.ts +++ b/yarn-project/aztec/src/cli/cmds/compile.ts @@ -1,51 +1,24 @@ import type { LogFn } from '@aztec/foundation/log'; -import { execFileSync, spawn } from 'child_process'; +import { execFileSync } from 'child_process'; import type { Command } from 'commander'; -import { readFile, readdir, writeFile } from 'fs/promises'; -import { join } from 'path'; +import { readFile, writeFile } from 'fs/promises'; -/** Spawns a command with inherited stdio and rejects on non-zero exit. */ -function run(cmd: string, args: string[]): Promise { - return new Promise((resolve, reject) => { - const child = spawn(cmd, args, { stdio: 'inherit' }); - child.on('error', reject); - child.on('close', code => { - if (code !== 0) { - reject(new Error(`${cmd} exited with code ${code}`)); - } else { - resolve(); - } - }); - }); -} +import { readArtifactFiles } from './utils/artifacts.js'; +import { run } from './utils/spawn.js'; -/** Returns paths to contract artifacts in the target directory. - * Contract artifacts are identified by having a `functions` array in the JSON. - */ +/** Returns paths to contract artifacts in the target directory. */ async function collectContractArtifacts(): Promise { - let files: string[]; + let files; try { - files = await readdir('target'); + files = await readArtifactFiles('target'); } catch (err: any) { - if (err?.code === 'ENOENT') { + if (err?.message?.includes('does not exist')) { return []; } - throw new Error(`Failed to read target directory: ${err.message}`); - } - - const artifacts: string[] = []; - for (const file of files) { - if (!file.endsWith('.json')) { - continue; - } - const filePath = join('target', file); - const content = JSON.parse(await readFile(filePath, 'utf-8')); - if (Array.isArray(content.functions)) { - artifacts.push(filePath); - } + throw err; } - return artifacts; + return files.filter(f => Array.isArray(f.content.functions)).map(f => f.filePath); } /** Strips the `__aztec_nr_internals__` prefix from function names in contract artifacts. */ diff --git a/yarn-project/aztec/src/cli/cmds/profile.ts b/yarn-project/aztec/src/cli/cmds/profile.ts new file mode 100644 index 000000000000..d7248025074a --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile.ts @@ -0,0 +1,25 @@ +import type { LogFn } from '@aztec/foundation/log'; + +import type { Command } from 'commander'; + +import { profileFlamegraph } from './profile_flamegraph.js'; +import { profileGates } from './profile_gates.js'; + +export function injectProfileCommand(program: Command, log: LogFn): Command { + const profile = program.command('profile').description('Profile compiled Aztec artifacts.'); + + profile + .command('gates') + .argument('[target-dir]', 'Path to the compiled artifacts directory', './target') + .description('Display gate counts for all compiled Aztec artifacts in a target directory.') + .action((targetDir: string) => profileGates(targetDir, log)); + + profile + .command('flamegraph') + .argument('', 'Path to the compiled contract artifact JSON') + .argument('', 'Name of the contract function to profile') + .description('Generate a gate count flamegraph SVG for a contract function.') + .action((artifactPath: string, functionName: string) => profileFlamegraph(artifactPath, functionName, log)); + + return program; +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_flamegraph.test.ts b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.test.ts new file mode 100644 index 000000000000..4a91bf417717 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.test.ts @@ -0,0 +1,51 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { execFileSync } from 'child_process'; +import { existsSync, readFileSync, rmSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..'); +const CLI = join(PACKAGE_ROOT, 'dest/bin/index.js'); +const WORKSPACE = join(PACKAGE_ROOT, 'test/mixed-workspace'); +const TARGET = join(WORKSPACE, 'target'); +const CONTRACT_ARTIFACT = join(TARGET, 'simple_contract-SimpleContract.json'); + +describe('aztec profile flamegraph', () => { + const svgPath = join(TARGET, 'simple_contract-SimpleContract-private_function-flamegraph.svg'); + + beforeAll(() => { + rmSync(TARGET, { recursive: true, force: true }); + runCompile(); + runFlamegraph(CONTRACT_ARTIFACT, 'private_function'); + }, 300_000); + + afterAll(() => { + rmSync(TARGET, { recursive: true, force: true }); + }); + + it('generates a valid flamegraph SVG', () => { + expect(existsSync(svgPath)).toBe(true); + const content = readFileSync(svgPath, 'utf-8'); + expect(content).toContain(''); + }); +}); + +function runCompile() { + try { + execFileSync('node', [CLI, 'compile'], { cwd: WORKSPACE, stdio: 'pipe' }); + } catch (e: any) { + throw new Error(`compile failed:\n${e.stderr?.toString() ?? e.message}`); + } +} + +function runFlamegraph(artifactPath: string, functionName: string) { + try { + execFileSync('node', [CLI, 'profile', 'flamegraph', artifactPath, functionName], { + encoding: 'utf-8', + stdio: 'pipe', + }); + } catch (e: any) { + throw new Error(`profile flamegraph failed:\n${e.stderr?.toString() ?? e.message}`); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts new file mode 100644 index 000000000000..78b4743d715e --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_flamegraph.ts @@ -0,0 +1,63 @@ +import type { LogFn } from '@aztec/foundation/log'; + +import { readFile, rename, rm, writeFile } from 'fs/promises'; +import { basename, dirname, join } from 'path'; + +import { makeFunctionArtifact } from './profile_utils.js'; +import type { CompiledArtifact } from './utils/artifacts.js'; +import { run } from './utils/spawn.js'; + +/** Generates a gate count flamegraph SVG for a single contract function. */ +export async function profileFlamegraph(artifactPath: string, functionName: string, log: LogFn): Promise { + const raw = await readFile(artifactPath, 'utf-8'); + const artifact: CompiledArtifact = JSON.parse(raw); + + if (!Array.isArray(artifact.functions)) { + throw new Error(`${artifactPath} does not appear to be a contract artifact (no functions array)`); + } + + const func = artifact.functions.find(f => f.name === functionName); + if (!func) { + const available = artifact.functions.map(f => f.name).join(', '); + throw new Error(`Function "${functionName}" not found in artifact. Available: ${available}`); + } + if (func.is_unconstrained) { + throw new Error(`Function "${functionName}" is unconstrained and cannot be profiled`); + } + + const outputDir = dirname(artifactPath); + const contractName = basename(artifactPath, '.json'); + const functionArtifact = join(outputDir, `${contractName}-${functionName}.json`); + + try { + await writeFile(functionArtifact, makeFunctionArtifact(artifact, func)); + + const profiler = process.env.PROFILER_PATH ?? 'noir-profiler'; + const bb = process.env.BB ?? 'bb'; + + await run(profiler, [ + 'gates', + '--artifact-path', + functionArtifact, + '--backend-path', + bb, + '--backend-gates-command', + 'gates', + '--output', + outputDir, + '--scheme', + 'chonk', + '--include_gates_per_opcode', + ]); + + // noir-profiler names the SVG using the internal function name which + // retains the __aztec_nr_internals__ prefix in the bytecode metadata. + const srcSvg = join(outputDir, `__aztec_nr_internals__${functionName}_gates.svg`); + const destSvg = join(outputDir, `${contractName}-${functionName}-flamegraph.svg`); + await rename(srcSvg, destSvg); + + log(`Flamegraph written to ${destSvg}`); + } finally { + await rm(functionArtifact, { force: true }); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_gates.test.ts b/yarn-project/aztec/src/cli/cmds/profile_gates.test.ts new file mode 100644 index 000000000000..d933dc232f0f --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_gates.test.ts @@ -0,0 +1,58 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { execFileSync } from 'child_process'; +import { rmSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..'); +const CLI = join(PACKAGE_ROOT, 'dest/bin/index.js'); +const WORKSPACE = join(PACKAGE_ROOT, 'test/mixed-workspace'); +const TARGET = join(WORKSPACE, 'target'); + +describe('aztec profile gates', () => { + let gatesOutput: string; + + beforeAll(() => { + rmSync(TARGET, { recursive: true, force: true }); + runCompile(); + gatesOutput = runProfile('gates'); + }, 300_000); + + afterAll(() => { + rmSync(TARGET, { recursive: true, force: true }); + }); + + it('prints gate counts for both contract functions', () => { + expect(gatesOutput).toContain('simple_contract-SimpleContract::private_function'); + expect(gatesOutput).toContain('simple_contract-SimpleContract::another_private_function'); + }); + + it('prints gate counts for both plain circuits', () => { + expect(gatesOutput).toContain('simple_circuit'); + expect(gatesOutput).toContain('simple_circuit_2'); + }); + + it('gate counts are positive integers', () => { + const counts = [...gatesOutput.matchAll(/(\d[\d,]*)\s*$/gm)].map(m => parseInt(m[1].replace(/,/g, ''), 10)); + expect(counts.length).toBeGreaterThanOrEqual(4); + for (const count of counts) { + expect(count).toBeGreaterThan(0); + } + }); +}); + +function runCompile() { + try { + execFileSync('node', [CLI, 'compile'], { cwd: WORKSPACE, stdio: 'pipe' }); + } catch (e: any) { + throw new Error(`compile failed:\n${e.stderr?.toString() ?? e.message}`); + } +} + +function runProfile(subcommand: string) { + try { + return execFileSync('node', [CLI, 'profile', subcommand, TARGET], { encoding: 'utf-8', stdio: 'pipe' }); + } catch (e: any) { + throw new Error(`profile ${subcommand} failed:\n${e.stderr?.toString() ?? e.message}`); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_gates.ts b/yarn-project/aztec/src/cli/cmds/profile_gates.ts new file mode 100644 index 000000000000..19770ba91f1e --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_gates.ts @@ -0,0 +1,67 @@ +import { asyncPool } from '@aztec/foundation/async-pool'; +import type { LogFn } from '@aztec/foundation/log'; + +import { execFile as execFileCb } from 'child_process'; +import { rm } from 'fs/promises'; +import { promisify } from 'util'; + +import { MAX_CONCURRENT, discoverArtifacts } from './profile_utils.js'; + +const execFile = promisify(execFileCb); + +interface GateCountResult { + name: string; + gateCount: number; +} + +/** Parses circuit_size from bb gates JSON output: { "functions": [{ "circuit_size": N }] } */ +function parseGateCount(stdout: string): number { + const parsed = JSON.parse(stdout); + const size = parsed?.functions?.[0]?.circuit_size; + if (typeof size !== 'number') { + throw new Error('Failed to parse circuit_size from bb gates output'); + } + return size; +} + +/** Runs bb gates on a single artifact file and returns the gate count. */ +async function getGateCount(bb: string, artifactPath: string): Promise { + const { stdout } = await execFile(bb, ['gates', '--scheme', 'chonk', '-b', artifactPath]); + return parseGateCount(stdout); +} + +/** Profiles all compiled artifacts in a target directory and prints gate counts. */ +export async function profileGates(targetDir: string, log: LogFn): Promise { + const bb = process.env.BB ?? 'bb'; + const { artifacts, tmpDir } = await discoverArtifacts(targetDir); + + if (artifacts.length === 0) { + log('No artifacts found in target directory.'); + return; + } + + try { + const results: GateCountResult[] = await asyncPool(MAX_CONCURRENT, artifacts, async artifact => ({ + name: artifact.name, + gateCount: await getGateCount(bb, artifact.filePath), + })); + results.sort((a, b) => a.name.localeCompare(b.name)); + + if (results.length === 0) { + log('No constrained circuits found.'); + return; + } + + const maxNameLen = Math.max(...results.map(r => r.name.length)); + log(''); + log('Gate counts:'); + log('-'.repeat(maxNameLen + 16)); + for (const { name, gateCount } of results) { + log(`${name.padEnd(maxNameLen)} ${gateCount.toLocaleString().padStart(12)}`); + } + log('-'.repeat(maxNameLen + 16)); + log(`Total: ${results.length} circuit(s)`); + } finally { + await rm(tmpDir, { recursive: true, force: true }); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/profile_utils.ts b/yarn-project/aztec/src/cli/cmds/profile_utils.ts new file mode 100644 index 000000000000..e604419bc631 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/profile_utils.ts @@ -0,0 +1,58 @@ +import { mkdtemp, writeFile } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +import type { CompiledArtifact, ContractFunction } from './utils/artifacts.js'; +import { readArtifactFiles } from './utils/artifacts.js'; + +export const MAX_CONCURRENT = 4; + +export interface DiscoveredArtifact { + name: string; + filePath: string; + type: 'contract-function' | 'program'; +} + +/** + * Reads a target directory and returns a list of discovered artifacts with temp files + * created for contract functions. Caller must clean up tmpDir when done. + */ +export async function discoverArtifacts( + targetDir: string, +): Promise<{ artifacts: DiscoveredArtifact[]; tmpDir: string }> { + const files = await readArtifactFiles(targetDir); + const tmpDir = await mkdtemp(join(tmpdir(), 'aztec-profile-')); + const artifacts: DiscoveredArtifact[] = []; + + for (const file of files) { + if (Array.isArray(file.content.functions)) { + for (const func of file.content.functions) { + if (!func.bytecode || func.is_unconstrained) { + continue; + } + const name = `${file.name}::${func.name}`; + const tmpPath = join(tmpDir, `${file.name}-${func.name}.json`); + await writeFile(tmpPath, makeFunctionArtifact(file.content, func)); + artifacts.push({ name, filePath: tmpPath, type: 'contract-function' }); + } + } else if (file.content.bytecode) { + artifacts.push({ name: file.name, filePath: file.filePath, type: 'program' }); + } + } + + return { artifacts, tmpDir }; +} + +/** Extracts a contract function as a standalone program artifact JSON string. */ +export function makeFunctionArtifact(artifact: CompiledArtifact, func: ContractFunction) { + /* eslint-disable camelcase */ + return JSON.stringify({ + noir_version: artifact.noir_version, + hash: 0, + abi: func.abi, + bytecode: func.bytecode, + debug_symbols: func.debug_symbols, + file_map: artifact.file_map, + }); + /* eslint-enable camelcase */ +} diff --git a/yarn-project/aztec/src/cli/cmds/utils/artifacts.ts b/yarn-project/aztec/src/cli/cmds/utils/artifacts.ts new file mode 100644 index 000000000000..104bbed25145 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/utils/artifacts.ts @@ -0,0 +1,44 @@ +import { readFile, readdir } from 'fs/promises'; +import { join } from 'path'; + +export interface CompiledArtifact { + noir_version: string; + file_map: unknown; + functions: ContractFunction[]; + bytecode?: string; +} + +export interface ContractFunction { + name: string; + abi: unknown; + bytecode: string; + debug_symbols: unknown; + is_unconstrained?: boolean; +} + +export interface ArtifactFile { + name: string; + filePath: string; + content: CompiledArtifact; +} + +/** Reads all JSON artifact files from a target directory and returns their parsed contents. */ +export async function readArtifactFiles(targetDir: string): Promise { + let entries: string[]; + try { + entries = (await readdir(targetDir)).filter(f => f.endsWith('.json')); + } catch (err: any) { + if (err?.code === 'ENOENT') { + throw new Error(`Target directory '${targetDir}' does not exist. Compile first with 'aztec compile'.`); + } + throw err; + } + + const artifacts: ArtifactFile[] = []; + for (const file of entries) { + const filePath = join(targetDir, file); + const content = JSON.parse(await readFile(filePath, 'utf-8')) as CompiledArtifact; + artifacts.push({ name: file.replace('.json', ''), filePath, content }); + } + return artifacts; +} diff --git a/yarn-project/aztec/src/cli/cmds/utils/spawn.ts b/yarn-project/aztec/src/cli/cmds/utils/spawn.ts new file mode 100644 index 000000000000..53514e06d931 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/utils/spawn.ts @@ -0,0 +1,16 @@ +import { spawn } from 'child_process'; + +/** Spawns a command with inherited stdio and rejects on non-zero exit. */ +export function run(cmd: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: 'inherit' }); + child.on('error', reject); + child.on('close', code => { + if (code !== 0) { + reject(new Error(`${cmd} exited with code ${code}`)); + } else { + resolve(); + } + }); + }); +} diff --git a/yarn-project/aztec/test/mixed-workspace/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/Nargo.toml index 2ef7da4e84a0..774733593fd3 100644 --- a/yarn-project/aztec/test/mixed-workspace/Nargo.toml +++ b/yarn-project/aztec/test/mixed-workspace/Nargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["simple_contract", "simple_circuit"] +members = ["simple_contract", "simple_circuit", "simple_circuit_2"] diff --git a/yarn-project/aztec/test/mixed-workspace/simple_circuit_2/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/simple_circuit_2/Nargo.toml new file mode 100644 index 000000000000..94ba379302a1 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_circuit_2/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "simple_circuit_2" +authors = [""] +compiler_version = ">=0.25.0" +type = "bin" + +[dependencies] diff --git a/yarn-project/aztec/test/mixed-workspace/simple_circuit_2/src/main.nr b/yarn-project/aztec/test/mixed-workspace/simple_circuit_2/src/main.nr new file mode 100644 index 000000000000..a51a0a7f9cf0 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_circuit_2/src/main.nr @@ -0,0 +1,3 @@ +fn main(x: Field, y: Field) { + assert(x != y); +} diff --git a/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr b/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr index 43c4331608db..478d776dad92 100644 --- a/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr +++ b/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr @@ -8,4 +8,9 @@ pub contract SimpleContract { fn private_function() -> Field { 0 } + + #[external("private")] + fn another_private_function(x: Field) -> Field { + x + } } diff --git a/yarn-project/protocol-contracts/src/scripts/generate_data.ts b/yarn-project/protocol-contracts/src/scripts/generate_data.ts index 69d62cfe7631..58b8d8aa14c7 100644 --- a/yarn-project/protocol-contracts/src/scripts/generate_data.ts +++ b/yarn-project/protocol-contracts/src/scripts/generate_data.ts @@ -1,3 +1,7 @@ +// Reads compiled Noir artifacts for each protocol contract and derives their addresses, class IDs, +// bytecode commitments, and other deployment data, emitting everything as precomputed constants into +// `protocol_contract_data.ts`. This avoids clients repeating the expensive hashing at runtime and +// ensures a single source of truth for the protocol contracts hash enforced by circuits, P2P, and L1. import { CANONICAL_AUTH_REGISTRY_ADDRESS, CONTRACT_CLASS_REGISTRY_CONTRACT_ADDRESS, diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index 74b83fdb9f96..e3a776edac02 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -25,7 +25,7 @@ import type { PublicProcessorValidator, SequencerConfig, } from '@aztec/stdlib/interfaces/server'; -import type { DebugLog } from '@aztec/stdlib/logs'; +import { type DebugLog, type DebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs'; import { ProvingRequestType } from '@aztec/stdlib/proofs'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { @@ -140,6 +140,7 @@ export class PublicProcessor implements Traceable { telemetryClient: TelemetryClient = getTelemetryClient(), private log: Logger, private opts: Pick = {}, + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.metrics = new PublicProcessorMetrics(telemetryClient, 'PublicProcessor'); } @@ -293,6 +294,8 @@ export class PublicProcessor implements Traceable { returns = returns.concat(returnValues); debugLogs.push(...txDebugLogs); + this.debugLogStore.storeLogs(processedTx.hash.toString(), txDebugLogs); + totalPublicGas = totalPublicGas.add(processedTx.gasUsed.publicGas); totalBlockGas = totalBlockGas.add(processedTx.gasUsed.totalGas); totalSizeInBytes += txSize; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts index 646317a3d94a..8d1c29746334 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts @@ -19,10 +19,11 @@ export function createPublicTxSimulatorForBlockBuilding( globalVariables: GlobalVariables, telemetryClient: TelemetryClient, bindings?: LoggerBindings, + collectDebugLogs = false, ) { const config = PublicSimulatorConfig.from({ skipFeeEnforcement: false, - collectDebugLogs: false, + collectDebugLogs, collectHints: false, collectPublicInputs: false, collectStatistics: false, diff --git a/yarn-project/stdlib/src/logs/debug_log_store.ts b/yarn-project/stdlib/src/logs/debug_log_store.ts new file mode 100644 index 000000000000..f671c4967bca --- /dev/null +++ b/yarn-project/stdlib/src/logs/debug_log_store.ts @@ -0,0 +1,54 @@ +import type { TxReceipt } from '../tx/tx_receipt.js'; +import type { DebugLog } from './debug_log.js'; + +/** + * Store for debug logs emitted by public functions during transaction execution. + * + * Uses the Null Object pattern: production code uses NullDebugLogStore (no-op), while test mode uses + * InMemoryDebugLogStore (stores and serves logs). + */ +export interface DebugLogStore { + /** Store debug logs for a processed transaction. */ + storeLogs(txHash: string, logs: DebugLog[]): void; + /** Decorate a TxReceipt with any stored debug logs for the given tx. */ + decorateReceiptWithLogs(txHash: string, receipt: TxReceipt): void; + /** Whether debug log collection is enabled. */ + readonly isEnabled: boolean; +} + +/** No-op implementation for production mode. */ +export class NullDebugLogStore implements DebugLogStore { + storeLogs(_txHash: string, _logs: DebugLog[]): void { + return; + } + decorateReceiptWithLogs(_txHash: string, _receipt: TxReceipt): void { + return; + } + get isEnabled(): boolean { + return false; + } +} + +/** In-memory implementation for test mode that stores and serves debug logs. */ +export class InMemoryDebugLogStore implements DebugLogStore { + private map = new Map(); + + storeLogs(txHash: string, logs: DebugLog[]): void { + if (logs.length > 0) { + this.map.set(txHash, logs); + } + } + + decorateReceiptWithLogs(txHash: string, receipt: TxReceipt): void { + if (receipt.isMined()) { + const debugLogs = this.map.get(txHash); + if (debugLogs) { + receipt.debugLogs = debugLogs; + } + } + } + + get isEnabled(): boolean { + return true; + } +} diff --git a/yarn-project/stdlib/src/logs/index.ts b/yarn-project/stdlib/src/logs/index.ts index dafe33e376db..aba30077041d 100644 --- a/yarn-project/stdlib/src/logs/index.ts +++ b/yarn-project/stdlib/src/logs/index.ts @@ -12,5 +12,6 @@ export * from './shared_secret_derivation.js'; export * from './tx_scoped_l2_log.js'; export * from './message_context.js'; export * from './debug_log.js'; +export * from './debug_log_store.js'; export * from './tag.js'; export * from './siloed_tag.js'; diff --git a/yarn-project/stdlib/src/tx/tx_receipt.ts b/yarn-project/stdlib/src/tx/tx_receipt.ts index ec54694712d5..3b67b0057ba5 100644 --- a/yarn-project/stdlib/src/tx/tx_receipt.ts +++ b/yarn-project/stdlib/src/tx/tx_receipt.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { RevertCode } from '../avm/revert_code.js'; import { BlockHash } from '../block/block_hash.js'; +import { DebugLog } from '../logs/debug_log.js'; import { type ZodFor, schemas } from '../schemas/schemas.js'; import { TxHash } from './tx_hash.js'; @@ -57,6 +58,12 @@ export class TxReceipt { public blockHash?: BlockHash, /** The block number in which the transaction was included. */ public blockNumber?: BlockNumber, + /** + * Debug logs collected during public function execution. Served only when the node is in test mode and placed on + * the receipt only because it's a convenient place for it (the logs are printed out by the wallet when a mined + * tx receipt is obtained). + */ + public debugLogs?: DebugLog[], ) {} /** Returns true if the transaction was executed successfully. */ @@ -103,6 +110,7 @@ export class TxReceipt { blockHash: BlockHash.schema.optional(), blockNumber: BlockNumberSchema.optional(), transactionFee: schemas.BigInt.optional(), + debugLogs: z.array(DebugLog.schema).optional(), }) .transform(fields => TxReceipt.from(fields)); } @@ -115,6 +123,7 @@ export class TxReceipt { transactionFee?: bigint; blockHash?: BlockHash; blockNumber?: BlockNumber; + debugLogs?: DebugLog[]; }) { return new TxReceipt( fields.txHash, @@ -124,6 +133,7 @@ export class TxReceipt { fields.transactionFee, fields.blockHash, fields.blockNumber, + fields.debugLogs, ); } diff --git a/yarn-project/txe/src/oracle/interfaces.ts b/yarn-project/txe/src/oracle/interfaces.ts index 6eb466cd8d80..98dd9f4c0086 100644 --- a/yarn-project/txe/src/oracle/interfaces.ts +++ b/yarn-project/txe/src/oracle/interfaces.ts @@ -71,11 +71,13 @@ export interface ITxeExecutionOracle { args: Fr[], argsHash: Fr, isStaticCall: boolean, + jobId: string, ): Promise; txeExecuteUtilityFunction( targetContractAddress: AztecAddress, functionSelector: FunctionSelector, args: Fr[], + jobId: string, ): Promise; txePublicCallNewFlow( from: AztecAddress, @@ -83,4 +85,7 @@ export interface ITxeExecutionOracle { calldata: Fr[], isStaticCall: boolean, ): Promise; + // TODO(F-335): Drop this from here as it's not a real oracle handler - it's only called from + // RPCTranslator::txeGetPrivateEvents and never from Noir. + syncContractNonOracleMethod(contractAddress: AztecAddress, scope: AztecAddress, jobId: string): Promise; } diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index ca94b79c2c8d..45be8bbcf95c 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -107,7 +107,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl private senderAddressBookStore: SenderAddressBookStore, private capsuleStore: CapsuleStore, private privateEventStore: PrivateEventStore, - private jobId: string, private nextBlockTimestamp: bigint, private version: Fr, private chainId: Fr, @@ -172,6 +171,25 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl return { txHash: txEffects.txHash, noteHashes: txEffects.noteHashes, nullifiers: txEffects.nullifiers }; } + async syncContractNonOracleMethod(contractAddress: AztecAddress, scope: AztecAddress, jobId: string) { + if (contractAddress.equals(DEFAULT_ADDRESS)) { + this.logger.debug(`Skipping sync in txeGetPrivateEvents because the events correspond to the default address.`); + return; + } + + const blockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); + await this.stateMachine.contractSyncService.ensureContractSynced( + contractAddress, + null, + async (call, execScopes) => { + await this.executeUtilityCall(call, execScopes, jobId); + }, + blockHeader, + jobId, + [scope], + ); + } + async txeGetPrivateEvents(selector: EventSelector, contractAddress: AztecAddress, scope: AztecAddress) { return ( await this.privateEventStore.getPrivateEvents(selector, { @@ -285,6 +303,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl args: Fr[], argsHash: Fr = Fr.zero(), isStaticCall: boolean = false, + jobId: string, ) { this.logger.verbose( `Executing external function ${await this.contractStore.getDebugFunctionName(targetContractAddress, functionSelector)}@${targetContractAddress} isStaticCall=${isStaticCall}`, @@ -304,7 +323,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl // Sync notes before executing private function to discover notes from previous transactions const utilityExecutor = async (call: FunctionCall, execScopes: AccessScopes) => { - await this.executeUtilityCall(call, execScopes); + await this.executeUtilityCall(call, execScopes, jobId); }; const blockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); @@ -313,7 +332,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl functionSelector, utilityExecutor, blockHeader, - this.jobId, + jobId, effectiveScopes, ); @@ -360,7 +379,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl capsuleStore: this.capsuleStore, privateEventStore: this.privateEventStore, contractSyncService: this.stateMachine.contractSyncService, - jobId: this.jobId, + jobId, totalPublicCalldataCount: 0, sideEffectCounter: minRevertibleSideEffectCounter, scopes: effectiveScopes, @@ -659,7 +678,12 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl return returnValues ?? []; } - async txeExecuteUtilityFunction(targetContractAddress: AztecAddress, functionSelector: FunctionSelector, args: Fr[]) { + async txeExecuteUtilityFunction( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + args: Fr[], + jobId: string, + ) { const artifact = await this.contractStore.getFunctionArtifact(targetContractAddress, functionSelector); if (!artifact) { throw new Error(`Cannot call ${functionSelector} as there is no artifact found at ${targetContractAddress}.`); @@ -671,10 +695,10 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl targetContractAddress, functionSelector, async (call, execScopes) => { - await this.executeUtilityCall(call, execScopes); + await this.executeUtilityCall(call, execScopes, jobId); }, blockHeader, - this.jobId, + jobId, 'ALL_SCOPES', ); @@ -689,10 +713,10 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl returnTypes: [], }); - return this.executeUtilityCall(call, 'ALL_SCOPES'); + return this.executeUtilityCall(call, 'ALL_SCOPES', jobId); } - private async executeUtilityCall(call: FunctionCall, scopes: AccessScopes): Promise { + private async executeUtilityCall(call: FunctionCall, scopes: AccessScopes, jobId: string): Promise { const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); if (entryPointArtifact.functionType !== FunctionType.UTILITY) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`); @@ -719,7 +743,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl senderAddressBookStore: this.senderAddressBookStore, capsuleStore: this.capsuleStore, privateEventStore: this.privateEventStore, - jobId: this.jobId, + jobId, scopes, }); const acirExecutionResult = await new WASMSimulator() diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index b81be2164ca9..95995654f675 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -285,6 +285,13 @@ export class RPCTranslator { const contractAddress = addressFromSingle(foreignContractAddress); const scope = addressFromSingle(foreignScope); + // TODO(F-335): Avoid doing the following 2 calls here. + { + await this.handlerAsTxe().syncContractNonOracleMethod(contractAddress, scope, this.stateHandler.getCurrentJob()); + // We cycle job to commit the stores after the contract sync. + await this.stateHandler.cycleJob(); + } + const events = await this.handlerAsTxe().txeGetPrivateEvents(selector, contractAddress, scope); if (events.length > MAX_PRIVATE_EVENTS_PER_TXE_QUERY) { @@ -1038,8 +1045,11 @@ export class RPCTranslator { args, argsHash, isStaticCall, + this.stateHandler.getCurrentJob(), ); + // TODO(F-335): Avoid doing the following call here. + await this.stateHandler.cycleJob(); return toForeignCallResult([toArray(returnValues)]); } @@ -1056,8 +1066,11 @@ export class RPCTranslator { targetContractAddress, functionSelector, args, + this.stateHandler.getCurrentJob(), ); + // TODO(F-335): Avoid doing the following call here. + await this.stateHandler.cycleJob(); return toForeignCallResult([toArray(returnValues)]); } @@ -1074,6 +1087,8 @@ export class RPCTranslator { const returnValues = await this.handlerAsTxe().txePublicCallNewFlow(from, address, calldata, isStaticCall); + // TODO(F-335): Avoid doing the following call here. + await this.stateHandler.cycleJob(); return toForeignCallResult([toArray(returnValues)]); } diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index 157f10bb1983..5c7b87ea4feb 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -113,6 +113,10 @@ export interface TXESessionStateHandler { enterPublicState(contractAddress?: AztecAddress): Promise; enterPrivateState(contractAddress?: AztecAddress, anchorBlockNumber?: BlockNumber): Promise; enterUtilityState(contractAddress?: AztecAddress): Promise; + + // TODO(F-335): Exposing the job info is abstraction breakage - drop the following 2 functions. + cycleJob(): Promise; + getCurrentJob(): string; } /** @@ -193,7 +197,6 @@ export class TXESession implements TXESessionStateHandler { senderAddressBookStore, capsuleStore, privateEventStore, - initialJobId, nextBlockTimestamp, version, chainId, @@ -254,6 +257,17 @@ export class TXESession implements TXESessionStateHandler { } } + getCurrentJob(): string { + return this.currentJobId; + } + + /** Commits the current job and begins a new one. Returns the new job ID. */ + async cycleJob(): Promise { + await this.jobCoordinator.commitJob(this.currentJobId); + this.currentJobId = this.jobCoordinator.beginJob(); + return this.currentJobId; + } + async enterTopLevelState() { switch (this.state.name) { case 'PRIVATE': { @@ -277,8 +291,7 @@ export class TXESession implements TXESessionStateHandler { } // Commit all staged stores from the job that was just completed, then begin a new job - await this.jobCoordinator.commitJob(this.currentJobId); - this.currentJobId = this.jobCoordinator.beginJob(); + await this.cycleJob(); this.oracleHandler = new TXEOracleTopLevelContext( this.stateMachine, @@ -292,7 +305,6 @@ export class TXESession implements TXESessionStateHandler { this.senderAddressBookStore, this.capsuleStore, this.privateEventStore, - this.currentJobId, this.nextBlockTimestamp, this.version, this.chainId, diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 7805f431610b..04ffeaf33fd8 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -28,6 +28,7 @@ import { type PublicProcessorLimits, type WorldStateSynchronizer, } from '@aztec/stdlib/interfaces/server'; +import { type DebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx'; import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; @@ -52,6 +53,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { private dateProvider: DateProvider, private telemetryClient: TelemetryClient, bindings?: LoggerBindings, + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.log = createLogger('checkpoint-builder', { ...bindings, @@ -152,6 +154,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()); const guardedFork = new GuardedMerkleTreeOperations(fork); + const collectDebugLogs = this.debugLogStore.isEnabled; + const bindings = this.log.getBindings(); const publicTxSimulator = createPublicTxSimulatorForBlockBuilding( guardedFork, @@ -159,6 +163,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { globalVariables, this.telemetryClient, bindings, + collectDebugLogs, ); const processor = new PublicProcessor( @@ -170,6 +175,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { this.telemetryClient, createLogger('simulator:public-processor', bindings), this.config, + this.debugLogStore, ); const validator = createValidatorForBlockBuilding( @@ -197,6 +203,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { private contractDataSource: ContractDataSource, private dateProvider: DateProvider, private telemetryClient: TelemetryClient = getTelemetryClient(), + private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { this.log = createLogger('checkpoint-builder'); } @@ -251,6 +258,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.dateProvider, this.telemetryClient, bindings, + this.debugLogStore, ); } @@ -311,6 +319,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.dateProvider, this.telemetryClient, bindings, + this.debugLogStore, ); } diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index d6ac9462f8be..f45118ed2773 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -28,7 +28,7 @@ import type { ChainInfo } from '@aztec/entrypoints/interfaces'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; import type { FieldsOf } from '@aztec/foundation/types'; -import type { AccessScopes, ContractNameResolver } from '@aztec/pxe/client/lazy'; +import { type AccessScopes, displayDebugLogs } from '@aztec/pxe/client/lazy'; import type { PXE, PackedPrivateEvent } from '@aztec/pxe/server'; import { type ContractArtifact, @@ -338,15 +338,6 @@ export abstract class BaseWallet implements Wallet { blockHeader = (await this.aztecNode.getBlockHeader())!; } - const getContractName: ContractNameResolver = async address => { - const instance = await this.pxe.getContractInstance(address); - if (!instance) { - return undefined; - } - const artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); - return artifact?.name; - }; - const [optimizedResults, normalResult] = await Promise.all([ optimizableCalls.length > 0 ? simulateViaNode( @@ -357,7 +348,7 @@ export abstract class BaseWallet implements Wallet { feeOptions.gasSettings, blockHeader, opts.skipFeeEnforcement ?? true, - getContractName, + this.getContractName.bind(this), ) : Promise.resolve([]), remainingCalls.length > 0 @@ -410,7 +401,27 @@ export abstract class BaseWallet implements Wallet { // Otherwise, wait for the full receipt (default behavior on wait: undefined) const waitOpts = typeof opts.wait === 'object' ? opts.wait : undefined; - return (await waitForTx(this.aztecNode, txHash, waitOpts)) as SendReturn; + const receipt = await waitForTx(this.aztecNode, txHash, waitOpts); + + // Display debug logs from public execution if present (served in test mode only) + if (receipt.debugLogs?.length) { + await displayDebugLogs(receipt.debugLogs, this.getContractName.bind(this)); + } + + return receipt as SendReturn; + } + + /** + * Resolves a contract address to a human-readable name via PXE, if available. + * @param address - The contract address to resolve. + */ + protected async getContractName(address: AztecAddress): Promise { + const instance = await this.pxe.getContractInstance(address); + if (!instance) { + return undefined; + } + const artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); + return artifact?.name; } protected contextualizeError(err: Error, ...context: string[]): Error { diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index bcb373985b4c..e1f69cf6ad09 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -180,10 +180,15 @@ export class EmbeddedWallet extends BaseWallet { const accountManager = await AccountManager.create(this, secret, contract, salt); const instance = accountManager.getInstance(); - const artifact = await accountManager.getAccountContract().getContractArtifact(); - - await this.registerContract(instance, artifact, accountManager.getSecretKey()); - + const existingInstance = await this.pxe.getContractInstance(instance.address); + if (!existingInstance) { + const existingArtifact = await this.pxe.getContractArtifact(instance.currentContractClassId); + await this.registerContract( + instance, + !existingArtifact ? await accountManager.getAccountContract().getContractArtifact() : undefined, + accountManager.getSecretKey(), + ); + } return accountManager; }