feat: immutables macro#28
Conversation
# 🤖 Linear Closes AZT-678 # Description Add initializerless constants pattern for Aztec Noir contracts. This pattern allows contracts to store immutable values committed via the contract's salt, eliminating the need for an initializer transaction. ## Changes ### Initializerless Library (`src/nr/initializerless`) - `#[constants]` comptime macro that generates: - `Serialize` / `Deserialize` implementations (via `derive_serialize_if_not_implemented` / `derive_deserialize_if_not_implemented`) - `Constants::init(context)` — loads from capsule and verifies against `instance.salt` - `Constants::init_unconstrained(context)` — unconstrained load without verification - Salt derivation: `salt = poseidon2_hash([actual_salt, ...serialized_constants])` - Deterministic `CONSTANTS_SLOT` computed from `poseidon2_hash_bytes("CONSTANTS_SLOT")` ### New Contracts - **`constants_contract`** — Demo contract with both constants and mutable storage, showcasing mixed usage - **`schnorr_constants_account_contract`** — Account contract using the initializerless pattern (signing key committed via salt, no initializer needed) - **`schnorr_account_contract`** — Standard Schnorr account with initializer-based key storage (used as baseline for comparison in tests/benchmarks) ### Infrastructure - Upgrade to Aztec `v4.0.0-nightly.20260204` - CI workflow updates for test runner
# Description - Rename macro library: `initializerless` → `constants` - Rename macro file: `constants.nr` → `macro.nr` - Rename contract: `schnorr_constants_account_contract` → `schnorr_initializerless_account_contract` - Update all `Nargo.toml` dependencies and workspace members to match new names - Update all `use initializerless::constants` → `use constants::constants` across contracts - Rewrite README with full usage guide, how-it-works explanation, and project structure --------- Signed-off-by: frov <frov@wonderland.xyz>
# Description - Renamed constants → immutables across the entire codebase: Noir libraries, contracts, directory names, file names, artifacts, config files, and README - Added capsule usage documentation (Section 4 in README) showing how to attach capsules to transactions - Bumped aztec dependency from v4.0.0-nightly.20260204 to v4.0.0-devnet.1-patch.0 (Nargo.toml + package.json) - Fixed breaking changes from the version bump: - #[nophasecheck] → #[allow_phase_change] (renamed in aztec-nr) - use dep::aztec:: → use aztec:: (Noir compiler dropped the dep:: prefix) --------- Signed-off-by: frov <frov@wonderland.xyz> Co-authored-by: Paperclip Minimizer <minim@wonderland.xyz>
…ests and benchmarks (#9) # 🤖 Linear Closes AZT-707 - Add comprehensive TypeScript test suite (E2E, unit, integration) for the `#[immutables]` macro pattern - Add TypeScript utilities for deploying contracts with immutables (salt computation, capsule creation, PXE registration) - Add `SchnorrInitializerlessAccountContract` TypeScript integration (AccountContract impl, AuthWitnessProvider, wallet registration) - Add standard `SchnorrAccount` utilities for comparison testing - Add CI workflow for building `aztec-standards` artifacts - Remove duplicate Noir contracts (`initializerless/`, `schnorr_immutables_account_contract/`) consolidated in `feat/constant-account` - Update README with TypeScript utilities docs, published vs unpublished deployment, and `#[noinitcheck]` explanation ## Details ### TypeScript Utilities (`src/ts/immutables/utils.ts`) Generic module for deploying **any** contract using the `#[immutables]` macro: - `computeContractSalt(actualSalt, serializedImmutables)` — derives salt via `poseidon2Hash` - `createImmutablesCapsule(address, actualSalt, fields)` — creates capsule for function calls - `deployWithImmutables(wallet, artifact, fields, options?)` — full deployment: instance creation, PXE registration, class + instance publication - Supports both published (on-chain) and unpublished (PXE-only) deployment via `skipInstancePublication` ### Account Contract Integration (`src/ts/schnorr-initializerless-account/`) - `SchnorrInitializerlessAccountContract` — implements `AccountContract` interface (no initializer, salt-based key verification) - `SchnorrInitializerlessAuthWitnessProvider` — Schnorr signature creation for tx authorization - `registerInitializerlessAccount(wallet, options?)` — one-call deployment + wallet registration with key derivation - `computeSchnorrAccountAddress(signingKey, options?)` — pre-compute address before deployment ### Tests | File | Description | Tests | |------|-------------|-------| | `schnorr-initializerless-account.test.ts` | Immutables pattern for account contracts | 12 tests: deploy + read key, different keys/salts → different addresses, wrong capsule/actualSalt rejection, published + unpublished variants | | `immutables-contract.test.ts` | Immutables + storage coexistence | Published/unpublished deploy, wrong capsule rejection, mixed usage with initializer | | `e2e.test.ts` | End-to-end with Dripper FPC | Initializerless account receives private tokens via drip, transfers, balance checks — compared against standard SchnorrAccount | ### CI - `scripts/build-aztec-standards.ts` — builds external `aztec-standards` token/dripper artifacts needed for E2E tests - Updated `tests.yml` workflow to run build step before tests ### Noir cleanup Removed duplicate contracts that were consolidated in the base branch: - `src/nr/initializerless/` (duplicate of `src/nr/immutables/`) - `src/nr/schnorr_immutables_account_contract/` (duplicate of `src/nr/schnorr_initializerless_account_contract/`) <!--start_gitstream_placeholder--> ### ✨ PR Description Purpose: Add comprehensive production infrastructure for initializerless immutables pattern with TypeScript SDK, benchmarks, and automated deployment workflows. Main changes: - Implemented persistent CapsuleStore integration with `store_immutables` utility and artifact introspection via `#[abi(immutables)]` layout generation - Added complete TypeScript SDK with `deployWithImmutables`, `serializeFromLayout`, and account contract integration for SchnorrInitializerlessAccount - Configured CI/CD pipelines for PR benchmarks, pre-release publishing, and baseline tracking with aztec-benchmark integration _Generated by LinearB AI and added by gitStream._ <sub>AI-generated content may contain inaccuracies. Please verify before using. 💡 **Tip:** You can customize your AI Description using **Guidelines** [Learn how](https://docs.gitstream.cm/automation-actions/#describe-changes)</sub> <!--end_gitstream_placeholder--> --------- Signed-off-by: frov <frov@wonderland.xyz> Co-authored-by: Weißer Hase <84595958+wei3erHase@users.noreply.github.com> Co-authored-by: Weißer Hase <wei3erHase@protonmail.com>
# Description - Add Noir TXE test scaffolding for `immutables_contract` and `schnorr_initializerless_account_contract` - Most tests are disabled because the TXE does not support deploying contracts with a custom salt ([aztec-packages#16656](AztecProtocol/aztec-packages#16656)), which is required for `Immutables::init()` salt verification - One storage-only test (`increment_counter_success`) runs successfully since it doesn't depend on salt verification - Update README with test directory structure and TXE limitation note
|
Caution Review failedFailed to post review comments WalkthroughAdds an immutables Noir macro, initializerless Schnorr account and supporting Noir contracts, TypeScript immutables/account tooling and E2E tests, replaces counter benchmarks/tests with account-focused suites, updates package metadata and CI to reusable workflows, and removes sandbox/version-check scripts and some in-repo workflows. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client (TS)
participant Wallet as Wallet/Deployer
participant Chain as Blockchain/Node
participant Macro as Immutables Macro (Noir)
participant PXE as PXE Capsule Store
Client->>Wallet: serialize immutables -> Fr[]
Client->>Wallet: computeContractSalt(actualSalt, serialized)
Client->>Wallet: createImmutablesCapsule(address?, actualSalt, serialized)
Wallet->>Chain: deployWithImmutables(instance, publish?)
Chain->>Macro: on-deploy (generated init/load paths)
Macro->>PXE: store or load capsuleData
PXE-->>Macro: capsuleData
Client->>Chain: send tx to account/contract
Chain->>Macro: Immutables::init(context) -> load & verify capsule via PXE
Macro-->>Chain: deserialized immutables (public key)
Chain-->>Client: tx result
sequenceDiagram
participant Caller as Client (TS)
participant Contract as ImmutablesContract (Noir)
participant Capsule as Capsule Storage (PXE)
participant Verifier as Immutables::init (Noir)
Caller->>Contract: store_immutables(capsuleData)
Contract->>Contract: computedSalt = poseidon2_hash(capsuleData)
Contract->>Contract: assert computedSalt == contractSalt
Contract->>Capsule: persist capsuleData
Capsule-->>Contract: ok
Caller->>Contract: get_signing_key()
Contract->>Verifier: Immutables::init(context)
Verifier->>Capsule: load capsuleData
Capsule-->>Verifier: capsuleData
Verifier->>Verifier: computedSalt = hash(capsuleData)
Verifier->>Verifier: assert computedSalt == instanceSalt
Verifier-->>Contract: deserialized immutables
Contract-->>Caller: signing_key_x, signing_key_y
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Comment |
There was a problem hiding this comment.
20 issues found across 55 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/ts/immutables/index.ts">
<violation number="1" location="src/ts/immutables/index.ts:191">
P1: Immutables length validation occurs after `wallet.registerContract()`. If the check fails, the contract is already registered in PXE, leaving an inconsistent state. Move this validation before the `registerContract` call.</violation>
<violation number="2" location="src/ts/immutables/index.ts:260">
P2: Custom agent: **TypeScript & React Standards**
Avoid `as any` cast — use a proper type assertion or fix the type mismatch with `poseidon2Hash`. If the function accepts `Fr[]`, cast to the correct type (e.g., `as Fr[]`) or adjust the input to match the expected signature.</violation>
<violation number="3" location="src/ts/immutables/index.ts:309">
P1: Custom agent: **TypeScript & React Standards**
Use `unknown[]` instead of `any[]` for `initializerArgs`. The rule requires avoiding `any` and using `unknown` when the type is not known. Since this is a public interface, `any` propagates unchecked types to all callers.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:119">
P2: Incorrect example value for `N`. The text says `N = 3` for a struct with two `Field` values, but since the array is `[Field; 1 + N]` (where `1` is the `actual_salt`), `N` should be `2` (the serialized length of just the immutables). Using `N = 3` would produce `[Field; 4]`, which is wrong. The reference implementation below confirms this: `[Field; 3]` for a 2-field struct.</violation>
</file>
<file name="src/ts/schnorr-initializerless-account.test.ts">
<violation number="1" location="src/ts/schnorr-initializerless-account.test.ts:52">
P2: Custom agent: **Test File Standards**
All test descriptions in this file use "should", which violates the test naming guideline. Prefer imperative or declarative phrasing, e.g. `"produces different addresses for different signing keys"` or `"deploys account with signing key and reads it back"`.
This applies to all 9 `it(...)` blocks in the file.</violation>
<violation number="2" location="src/ts/schnorr-initializerless-account.test.ts:78">
P2: Custom agent: **Test File Standards**
This test conflates four distinct behaviors (non-zero check, idempotency, key sensitivity, salt sensitivity) into a single test case. Split into separate tests — e.g. `"computes non-zero salt"`, `"computes deterministic salt for same inputs"`, `"computes different salt for different keys"`, `"computes different salt for different actualSalt"`.</violation>
<violation number="3" location="src/ts/schnorr-initializerless-account.test.ts:167">
P2: These negative tests under the "Published" describe block deploy without `publishClass`/`publishInstance` flags, so they actually test the unpublished code path. Pass the same publish options as the other tests in this block to ensure the published path's capsule validation is exercised.</violation>
</file>
<file name="src/ts/utils.ts">
<violation number="1" location="src/ts/utils.ts:16">
P2: Custom agent: **TypeScript & React Standards**
Environment variable `NODE_URL` is read without runtime type-checking. The rule requires runtime validation for environment variables (e.g., using Zod). Consider defining a schema that validates the value is a proper URL string.</violation>
<violation number="2" location="src/ts/utils.ts:57">
P2: Custom agent: **TypeScript & React Standards**
Exported function `setupTestSuite` is missing an explicit return type. It returns a complex object with `node`, `wallet`, `accounts`, `sponsoredPaymentMethod`, and `cleanup` — define a named return type (e.g., `Promise<TestSuiteContext>`) so consumers get clear type documentation without relying on inference.</violation>
<violation number="3" location="src/ts/utils.ts:73">
P2: If `wallet.stop()` throws, the temp directory is never cleaned up. Wrap `wallet.stop()` in a try/finally (or its own try/catch) so `rmSync` always runs.</violation>
</file>
<file name="src/nr/schnorr_initializerless_account_contract/src/main.nr">
<violation number="1" location="src/nr/schnorr_initializerless_account_contract/src/main.nr:79">
P0: Missing `#[noinitcheck]` on `entrypoint`. This is an initializerless contract (no `#[initializer]`), so the deployment nullifier is never emitted. Without `#[noinitcheck]`, the `#[aztec]` macro's default init-check will reject every call to this function. Both the standard `SchnorrAccount` and the `ImmutablesContract` use `#[noinitcheck]` for the same reason.</violation>
<violation number="2" location="src/nr/schnorr_initializerless_account_contract/src/main.nr:94">
P0: Missing `#[noinitcheck]` on `verify_private_authwit`. Same issue as `entrypoint` — without it, the init-check will fail since no deployment nullifier exists for this initializerless contract.</violation>
<violation number="3" location="src/nr/schnorr_initializerless_account_contract/src/main.nr:188">
P1: Missing `#[noinitcheck]` on `get_signing_public_key`. Same init-check issue as the other private functions — this will fail at runtime without the attribute.</violation>
</file>
<file name="src/ts/schnorr-account/utils.ts">
<violation number="1" location="src/ts/schnorr-account/utils.ts:76">
P2: Custom agent: **TypeScript & React Standards**
Use `unknown` instead of `any` for `paymentMethod`. The rule states: "Avoid `any`; use `unknown` when necessary." If a more specific type from the Aztec SDK is available (e.g., `FeePaymentMethod`), prefer that; otherwise use `unknown`.</violation>
<violation number="2" location="src/ts/schnorr-account/utils.ts:85">
P2: Bug: `signingPublicKey` is computed from the private key scalar's `.lo`/`.hi` halves, not from an actual Grumpkin point derivation. `deriveSigningKey` returns a `Fq` scalar; splitting it into two halves does not produce curve point coordinates. You need to derive the public key by multiplying the scalar by the generator (e.g., `Schnorr.computePublicKey`).</violation>
</file>
<file name="src/ts/schnorr-initializerless-account/index.ts">
<violation number="1" location="src/ts/schnorr-initializerless-account/index.ts:282">
P0: Critical bug: `createSchnorrInitializerlessAccount` uses `GrumpkinScalar.fromHighLow(Fr.ZERO, secretKey)` for key derivation, but `deploySchnorrInitializerlessAccount` uses `deriveSigningKey(secretKey)`. These produce different signing keys from the same secret, making the two functions incompatible. The factory function should use the same derivation as the deploy function.</violation>
</file>
<file name="src/ts/e2e.test.ts">
<violation number="1" location="src/ts/e2e.test.ts:95">
P2: Custom agent: **Test File Standards**
All test descriptions use "should" — the rule explicitly prohibits this. Prefer declarative names like `"mints to private balance of initializerless account"`, `"allows unpublished account to send private transactions"`, etc. This applies to all 7 `it(...)` blocks in this file.</violation>
<violation number="2" location="src/ts/e2e.test.ts:227">
P1: Custom agent: **Test File Standards**
This test conflates multiple behaviors: three distinct transfers (private→private, private→public, and reverse private→private), intermediate balance checks after each, and final balance verification for four balances. Break this into separate tests — e.g., one for initializerless→standard transfer, one for private→public self-transfer, one for standard→initializerless transfer. Each test can set up its own accounts and verify a single transfer behavior.</violation>
</file>
<file name="src/ts/immutables-contract.test.ts">
<violation number="1" location="src/ts/immutables-contract.test.ts:44">
P2: Custom agent: **Test File Standards**
All 12 test descriptions use the word "should", which the test file standards rule explicitly prohibits. Use imperative/declarative phrasing instead — e.g., `"produces different addresses for different actualSalt"`, `"rejects store_immutables with wrong data"`, `"deploys contract with immutables and reads them back"`.</violation>
<violation number="2" location="src/ts/immutables-contract.test.ts:61">
P2: Custom agent: **Test File Standards**
This test conflates four independent behaviors (non-zero salt, determinism, immutables-sensitivity, actualSalt-sensitivity) into a single test case. Per the test standards rule, each test should test one behavior. Split into separate tests like `"computes non-zero salt"`, `"computes deterministic salt for same inputs"`, `"computes different salt for different immutables"`, and `"computes different salt for different actualSalt"`.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Benchmark Comparison
Contract: account
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Benchmark Comparison
Contract: account
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="README.md">
<violation number="1" location="README.md:133">
P3: The README examples now require a `deployer` argument but never show how to define it (e.g., `const deployer = wallet.getAddress();`), so the snippets won’t run as written.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
why are we using aztec-standards token instead of using the counter for testing / benchmarking?
There was a problem hiding this comment.
the repo started from a previous one, but agree it's unnecessary as counter covers public and private fns. Then only added value of token is testing with notes, which is nice for wallet implementations.
| use crate::ImmutablesContract; | ||
| use crate::test::utils::setup; | ||
|
|
||
| // TODO: The TXE (Test Execution Environment) does not support deploying contracts |
There was a problem hiding this comment.
do we have an open issue on their repo to highlight this need?
There was a problem hiding this comment.
They just closed it yesterday feat: add salt and secret params to env.deploy, we'll be able to test this in the version that includes it (also useful for escrow tests as the logic contract address is the salt of the escrow contract)
| // Increment counter via public function | ||
| await contract.methods.increment_counter().send({ from: alice }); |
There was a problem hiding this comment.
how come we're not using the previous counter and adding some methods, instead of redeclaring it on the "MixedBehaviour" contract?
There was a problem hiding this comment.
the repo started from a previous one, was kind of a merging with boilerplate, but agree that would have been better
bd05afa
Benchmark Comparison
Contract: account
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
🤖 Linear
Closes AZT-XXX
Description
This branch introduces the
#[immutables]macro pattern for Aztec Noir contracts — a mechanism that allows contracts to store immutable values committed via the contract's salt, eliminating the need foran initializer transaction. It includes the Noir library, example contracts, a full TypeScript SDK, tests, benchmarks, and CI infrastructure.
Noir Library & Contracts (PRs #8, #16, #19, #27)
#[immutables]comptime macro (src/nr/immutables/src/macro.nr) that generates:Serialize/DeserializeimplementationsImmutables::init(context)— loads from capsule and verifies againstinstance.saltImmutables::init_unconstrained(context)— unconstrained load without verificationsalt = poseidon2_hash([actual_salt, ...serialized_immutables])immutables_contract— Demo contract with both immutables and mutable storage (mixed usage)schnorr_initializerless_account_contract— Account contract using the initializerless pattern (signing key committed via salt, no initializer needed)schnorr_account_contract— Standard Schnorr account with initializer-based key storage (baseline for comparison)v4.0.0-devnet.1-patch.0TypeScript SDK (PR #9)
src/ts/immutables/— Generic utilities for deploying any contract using#[immutables]:computeContractSalt(actualSalt, serializedImmutables)createImmutablesCapsule(address, actualSalt, fields)deployWithImmutables(wallet, artifact, fields, options?)src/ts/schnorr-initializerless-account/— Account contract integration:SchnorrInitializerlessAccountContract(implementsAccountContractinterface)SchnorrInitializerlessAuthWitnessProvider(Schnorr signature creation)registerInitializerlessAccount(wallet, options?)— one-call deployment + wallet registrationcomputeSchnorrAccountAddress(signingKey, options?)— pre-compute address before deploymentTests & Benchmarks (PR #9)
schnorr-initializerless-account.test.tsimmutables-contract.test.tse2e.test.tsaccount.benchmark.tsDocumentation & CI (PRs #16, #19)
Summary by CodeRabbit
New Features
Documentation
Tests
Chores