Migration notes
Aztec is in active development. Each version may introduce breaking changes that affect compatibility with previous versions. This page documents common errors and difficulties you might encounter when upgrading, along with guidance on how to resolve them.
4.2.0
[Aztec.js] GasSettings.default() renamed to GasSettings.fallback()
GasSettings.default() has been renamed to GasSettings.fallback() to clarify that these gas limits are not protocol defaults — the protocol has no concept of "default" gas settings. fallback() is a convenience for cases where gas estimation is not being used, but callers should prefer estimating gas via simulation for accurate limits.
The old DEFAULT_GAS_LIMIT and DEFAULT_TEARDOWN_GAS_LIMIT constants have been removed. Gas limits are now derived from protocol-level maximums (MAX_PROCESSABLE_L2_GAS, MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT) rather than arbitrary fixed values.
A new GasSettings.forEstimation() method provides intentionally high gas limits for use during simulation. These limits exceed protocol maximums so the simulation doesn't hit gas caps — you must pass skipTxValidation: true when simulating with them, then use the results to set accurate gas limits on the actual transaction. EmbeddedWallet does this by default.
Migration:
- import { DEFAULT_GAS_LIMIT, DEFAULT_TEARDOWN_GAS_LIMIT } from '@aztec/constants';
- const settings = GasSettings.default({ maxFeesPerGas });
+ const settings = GasSettings.fallback({ maxFeesPerGas });
Impact: Any code referencing GasSettings.default(), DEFAULT_GAS_LIMIT, or DEFAULT_TEARDOWN_GAS_LIMIT will fail to compile.
[PXE] simulateTx, executeUtility, profileTx, and proveTx no longer accept scopes: 'ALL_SCOPES'
The AccessScopes type ('ALL_SCOPES' | AztecAddress[]) has been removed. The scopes field in SimulateTxOpts,
ExecuteUtilityOpts, and ProfileTxOpts now requires an explicit AztecAddress[]. Callers that previously passed
'ALL_SCOPES' must now specify which addresses will be in scope for the call.
Migration:
+ const accounts = await pxe.getRegisteredAccounts();
+ const scopes = accounts.map(a => a.address);
// simulateTx
- await pxe.simulateTx(txRequest, { simulatePublic: true, scopes: 'ALL_SCOPES' });
+ await pxe.simulateTx(txRequest, { simulatePublic: true, scopes });
// executeUtility
- await pxe.executeUtility(call, { scopes: 'ALL_SCOPES' });
+ await pxe.executeUtility(call, { scopes });
// profileTx
- await pxe.profileTx(txRequest, { profileMode: 'full', scopes: 'ALL_SCOPES' });
+ await pxe.profileTx(txRequest, { profileMode: 'full', scopes });
// proveTx
- await pxe.proveTx(txRequest, 'ALL_SCOPES');
+ await pxe.proveTx(txRequest, scopes);
Impact: Any code passing 'ALL_SCOPES' to simulateTx, executeUtility, profileTx, or proveTx will fail to compile. Replace with an explicit array of account addresses.
[PXE] Capsule operations are now scope-enforced at the PXE level
The PXE now enforces that capsule operations can only access scopes that were authorized for the current execution. If a contract attempts to access a capsule scope that is not in its allowed scopes list, the PXE will throw an error:
Scope 0x1234... is not in the allowed scopes list: [0xabcd...].
The zero address (AztecAddress::zero()) is always allowed regardless of the scopes list, preserving backwards compatibility for contracts using the global scope.
Impact: Contracts that access capsules scoped to addresses not included in the transaction's authorized scopes will now fail at runtime. Ensure the correct scopes are passed when executing transactions.
[aztec.js] EmbeddedWalletOptions now uses a unified pxe field
The pxeConfig and pxeOptions fields on EmbeddedWalletOptions have been deprecated in favor of a single pxe field that accepts both PXE configuration and dependency overrides (custom prover, store, simulator):
const wallet = await EmbeddedWallet.create(nodeUrl, {
- pxeConfig: { proverEnabled: true },
- pxeOptions: { proverOrOptions: myCustomProver },
+ pxe: {
+ proverEnabled: true,
+ proverOrOptions: myCustomProver,
+ },
});
The old fields still work but will be removed in a future release.
[Aztec.nr] Ephemeral arrays replace capsule arrays in PXE oracle interfaces
Oracle interfaces between Aztec.nr and PXE now use a new EphemeralArray type (aztec::ephemeral::EphemeralArray) instead of CapsuleArray. Ephemeral arrays live in memory and are scoped by contract call frame, so they no longer need to be addressed by (contract_address, scope). Several public message-discovery and validation functions lost their recipient, scope, and contract_address parameters as a result.
Most contracts are not affected, as the macro-generated sync_state and process_message functions handle these APIs automatically. Only contracts that call these functions directly need to update.
Migration:
attempt_note_discovery(
contract_address,
tx_hash,
unique_note_hashes_in_tx,
first_nullifier_in_tx,
- recipient,
compute_note_hash,
compute_note_nullifier,
owner,
storage_slot,
randomness,
note_type_id,
packed_note,
);
- enqueue_note_for_validation(contract_address, owner, storage_slot, randomness, note_nonce, packed_note, note_hash, nullifier, tx_hash, scope);
+ enqueue_note_for_validation(contract_address, owner, storage_slot, randomness, note_nonce, packed_note, note_hash, nullifier, tx_hash);
- enqueue_event_for_validation(contract_address, event_type_id, randomness, serialized_event, event_commitment, tx_hash, scope);
+ enqueue_event_for_validation(contract_address, event_type_id, randomness, serialized_event, event_commitment, tx_hash);
- validate_and_store_enqueued_notes_and_events(contract_address, scope);
+ validate_and_store_enqueued_notes_and_events(scope);
The sync_inbox function and the OffchainInboxSync type now return EphemeralArray<OffchainMessageWithContext> instead of CapsuleArray<OffchainMessageWithContext>. Custom message handlers that bind the returned array to an explicit type must update the type annotation.
Impact: Contracts that call the above functions directly (rather than relying on macro-generated code) will fail to compile until the trailing recipient, scope, and contract_address parameters are removed.
4.2.0-aztecnr-rc.2
[Aztec.js] Removed SingleKeyAccountContract
The SchnorrSingleKeyAccount contract and its TypeScript wrapper SingleKeyAccountContract have been removed. This contract was insecure: it used ivpk_m (incoming viewing public key) as its Schnorr signing key, meaning anyone who received a user's viewing key could sign transactions on their behalf.
Migration:
- import { SingleKeyAccountContract } from '@aztec/accounts/single_key';
- const contract = new SingleKeyAccountContract(signingKey);
+ import { SchnorrAccountContract } from '@aztec/accounts/schnorr';
+ const contract = new SchnorrAccountContract(signingKey);
Impact: If you were using @aztec/accounts/single_key, switch to @aztec/accounts/schnorr which uses separate keys for encryption and authentication.
Custom token FPCs removed from default public setup allowlist
Token contract functions (like transfer_in_public and _increase_public_balance) have been removed from the default public setup allowlist. FPCs that accept custom tokens (like the reference FPC contract) will not work on public networks, because their setup-phase calls to these functions will be rejected. Token class IDs change with each aztec-nr release, making it impractical to maintain them in the allowlist.
FPCs that use only Fee Juice still work on all networks, since FeeJuice is a protocol contract with a fixed address in the allowlist. Custom FPCs should only call protocol contract functions (AuthRegistry, FeeJuice) during setup.
PublicFeePaymentMethod and PrivateFeePaymentMethod in aztec.js are affected, since they use the reference FPC contract which calls Token functions during setup. Switch to FeeJuicePaymentMethodWithClaim (after bridging Fee Juice from L1) or write an FPC that uses Fee Juice natively.
Migration:
- import { PublicFeePaymentMethod } from '@aztec/aztec.js/fee';
- const paymentMethod = new PublicFeePaymentMethod(fpcAddress, senderAddress, wallet, gasSettings);
+ import { FeeJuicePaymentMethodWithClaim } from '@aztec/aztec.js/fee';
+ const paymentMethod = new FeeJuicePaymentMethodWithClaim(senderAddress, claim);
Similarly, the fpc-public and fpc-private CLI wallet payment methods use the reference Token-based FPC and will not work on public networks. Use fee_juice for direct Fee Juice payment, or fpc-sponsored on devnet and local network.
[Aztec.nr] Domain-separated tags on log emission
All logs emitted through the Aztec.nr framework now include a domain-separated tag at fields[0]. Each log category uses its own domain separator via compute_log_tag(raw_tag, dom_sep):
- Events (
DOM_SEP__EVENT_LOG_TAG): the event type ID is the raw tag. - Message delivery (
DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG): the discovery tag is the raw tag. - Partial note completion logs (
DOM_SEP__NOTE_COMPLETION_LOG_TAG): the partial note'scommitmentfield is the raw tag.
The low-level emit methods now take tag as an explicit first parameter and have been renamed with an _unsafe suffix. Previously the tag was included as log[0] — it has now been extracted into its own parameter, and log no longer contains it:
- context.emit_private_log(log, length);
+ context.emit_private_log_unsafe(tag, log, length);
- context.emit_raw_note_log(log, length, note_hash_counter);
+ context.emit_raw_note_log_unsafe(tag, log, length, note_hash_counter);
- context.emit_public_log(log);
+ context.emit_public_log_unsafe(tag, log);
Prefer the higher-level APIs (emit for events, MessageDelivery for messages) which handle tagging automatically.
[Aztec.nr] Public events no longer include the event type selector at the end of the payload
emit_event_in_public previously appended the event type selector as the last field. It now prepends a domain-separated tag at fields[0] instead. The payload after the tag contains only the serialized event fields.
If you were reading public event directly from node logs (i.e. via node.getPublicLogs and not via wallet.getPublicEvents), update your parsing:
- // Old: fields = [serialized_event..., event_type_selector]
- const selector = EventSelector.fromField(fields[fields.length - 1]);
- const event = decodeFromAbi([abiType], fields);
+ // New: fields = [domain_separated_tag, serialized_event...]
+ const eventFields = log.getEmittedFieldsWithoutTag();
+ const event = decodeFromAbi([abiType], eventFields);
[Aztec.nr] Capsule operations are now addressed by scope
All capsule operations (store, load, delete, copy) and CapsuleArray now require a scope: AztecAddress parameter. This scopes capsule storage by address, providing isolation between different accounts within the same PXE.
Contracts that use CapsuleArray directly also need to update.
Migration:
- let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, slot);
+ let array: CapsuleArray<Field> = CapsuleArray::at(contract_address, slot, scope);
The low-level capsule functions are similarly affected:
- capsules::store(contract_address, slot, value);
+ capsules::store(contract_address, slot, value, scope);
- capsules::load(contract_address, slot);
+ capsules::load(contract_address, slot, scope);
- capsules::delete(contract_address, slot);
+ capsules::delete(contract_address, slot, scope);
- capsules::copy(contract_address, src_slot, dst_slot, num_entries);
+ capsules::copy(contract_address, src_slot, dst_slot, num_entries, scope);
If you need to stick the old, scope-less behavior, and you are really sure that that's what you need to use, you can use scope = AztecAddress::zero().
[Aztec.nr] process_message utility function removed
The auto-generated process_message utility function has been removed. If you need to deliver offchain messages (messages not broadcast via onchain logs), use the offchain_receive utility function instead. This function is automatically injected by the #[aztec] macro and accepts messages into a persistent inbox scoped by recipient. These messages are then picked up and processed during sync_state.
Impact: Contracts that explicitly called process_message must switch to delivering messages via offchain_receive and letting sync_state handle processing.
[Aztec.nr] CustomMessageHandler type signature changed
The CustomMessageHandler function type now receives an additional scope: AztecAddress parameter:
type CustomMessageHandler = unconstrained fn(
AztecAddress, // contract_address
u64, // msg_type_id
u64, // msg_metadata
BoundedVec<Field, MAX_MESSAGE_CONTENT_LEN>, // msg_content
MessageContext, // message_context
+ AztecAddress, // scope
);
Impact: Contracts that implement a custom message handler must update the function signature.
[aztec.js] isContractInitialized is now initializationStatus tri-state enum
ContractMetadata.isContractInitialized has been renamed to ContractMetadata.initializationStatus and changed from boolean | undefined to a ContractInitializationStatus enum with values INITIALIZED, UNINITIALIZED, and UNKNOWN.
INITIALIZED: the contract has been initialized (initialization nullifier found)UNINITIALIZED: the contract instance is registered but has not been initializedUNKNOWN: the instance is not registered and no public initialization nullifier was found
When the instance is not registered, the wallet now attempts to check the public initialization nullifier (computed from address alone) before returning UNKNOWN. Previously this case returned undefined.
Migration:
+ import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
const metadata = await wallet.getContractMetadata(address);
- if (metadata.isContractInitialized) {
+ if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) {
// contract is initialized
}
[Aztec.js] Use NO_FROM instead of AztecAddress.ZERO to bypass account contract entrypoint
When sending transactions that should not be mediated by an account contract (e.g., account contract self-deployments), use the explicit NO_FROM sentinel instead of AztecAddress.ZERO.
NO_FROM signals that the transaction should be executed directly via the DefaultEntrypoint. This replaces the brittle convention of passing AztecAddress.ZERO as the from field.
Migration:
- import { AztecAddress } from '@aztec/aztec.js';
+ import { NO_FROM } from '@aztec/aztec.js/account';
await contract.methods.my_method().send({
- from: AztecAddress.ZERO,
+ from: NO_FROM,
});
Note that DefaultEntrypoint only accepts a single call. If you need to execute multiple calls without account contract mediation (e.g., deploying an account contract and paying a fee in the same transaction), wrap them through DefaultMultiCallEntrypoint on the app side before sending:
import { NO_FROM } from "@aztec/aztec.js/account";
import { DefaultMultiCallEntrypoint } from "@aztec/entrypoints/multicall";
import { mergeExecutionPayloads } from "@aztec/stdlib/tx";
// Merge multiple execution payloads into one
const merged = mergeExecutionPayloads([deployPayload, feePayload]);
// Wrap through multicall so it becomes a single call for DefaultEntrypoint
const multicall = new DefaultMultiCallEntrypoint();
const chainInfo = await wallet.getChainInfo();
const wrappedPayload = await multicall.wrapExecutionPayload(merged, chainInfo);
// Send without account contract mediation
await wallet.sendTx(wrappedPayload, { from: NO_FROM });
Using other contracts for wrapping (for example, supporting more calls) is also supported, as long as the contract is registered in the wallet. This opens the door to different flows that do not use account entrypoints as the first call in the chain, including app sponsored FPCs.
Impact: Any code that passes AztecAddress.ZERO as the from option in .send(), .simulate(), or deploy options must switch to NO_FROM. Wallets use DefaultEntrypoint directly for NO_FROM transactions, instead of the DefaultMultiCallEntrypoint that was used internally before when specifying AztecAddress.ZERO.
[Aztec.js] ExecuteUtilityOptions.scope renamed to scopes and type changed to AztecAddress[]
The scope field in ExecuteUtilityOptions has been renamed to scopes and changed from a single AztecAddress to AztecAddress[]. This aligns the wallet's executeUtility API with the PXE API and sendTx in Wallet, which both accept an array of scopes.
Migration:
wallet.executeUtility(call, {
- scope: myAddress,
+ scopes: [myAddress],
});
Impact: Any code that calls wallet.executeUtility directly must update the options object. Wallets must update to adapt to the new interface
[Aztec.nr] attempt_note_discovery now takes two separate functions instead of one
The attempt_note_discovery function (and related discovery functions like do_sync_state, process_message_ciphertext) now takes separate compute_note_hash and compute_note_nullifier arguments instead of a single combined compute_note_hash_and_nullifier. The corresponding type aliases are now ComputeNoteHash and ComputeNoteNullifier (instead of ComputeNoteHashAndNullifier).
This split improves performance during nonce discovery: the note hash only needs to be computed once, while the old combined function recomputed it for every candidate nonce.
Most contracts are not affected, as the macro-generated sync_state and process_message functions handle this automatically. Only contracts that call attempt_note_discovery directly need to update.
Migration:
attempt_note_discovery(
contract_address,
tx_hash,
unique_note_hashes_in_tx,
first_nullifier_in_tx,
recipient,
- _compute_note_hash_and_nullifier,
+ _compute_note_hash,
+ _compute_note_nullifier,
owner,
storage_slot,
randomness,
note_type_id,
packed_note,
);
Impact: Contracts that call attempt_note_discovery or related discovery functions directly with a custom _compute_note_hash_and_nullifier argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Additionally, if you had a custom _compute_note_hash_and_nullifier function then compilation will now fail as you'll need to also produce the corresponding _compute_note_hash and _compute_note_nullifier functions.
Private initialization nullifier now includes init_hash
The private initialization nullifier is no longer derived from just the contract address. It is now computed as a Poseidon2 hash of [address, init_hash] using a dedicated domain separator. This prevents observers from determining whether a fully private contract has been initialized by simply knowing its address.
Note that Wallet.getContractMetadata now returns initializationStatus: ContractInitializationStatus.UNKNOWN when the wallet does not have the contract instance registered, since init_hash is needed to compute the nullifier and initialization status cannot be determined. Previously, this check worked for any address. Callers should check the enum value before branching on the initialization state.
If you use assert_contract_was_initialized_by or assert_contract_was_not_initialized_by from aztec::history::deployment, these now require an additional init_hash: Field parameter:
+ let instance = get_contract_instance(contract_address);
assert_contract_was_initialized_by(
block_header,
contract_address,
+ instance.initialization_hash,
);
Two separate init nullifiers for private and public
Contract initialization now emits two separate nullifiers instead of one: a private init nullifier and a public init nullifier. Each nullifier gates its respective execution domain:
- Private external functions check the private init nullifier.
- Public external functions check the public init nullifier.
How initializers work:
- Private initializers emit the private init nullifier. If the contract has any external public functions, the protocol auto-enqueues a public call to emit the public init nullifier.
- Public initializers emit both nullifiers directly.
- Contracts with no public functions only emit the private init nullifier.
only_self functions no longer have init checks. They behave as if marked noinitcheck.
External functions called during private initialization must be #[only_self]. Init nullifiers are emitted at the end of the initializer, so any external functions called on the initializing contract (e.g. via enqueue_self or call_self) during initialization will fail the init check unless they skip it.
Breaking change for deployment: If your contract has external public functions and a private initializer, the class must be registered onchain before initialization. You can no longer pass skipClassPublication: true, because the auto-enqueued public call requires the class to be available.
const deployed = await MyContract.deploy(wallet, ...args).send({
- skipClassPublication: true,
}).deployed();
[Aztec.nr] Made compute_note_hash_for_nullification unconstrained
This function shouldn't have been constrained in the first place, as constrained computation of HintedNote nullifiers is dangerous (constrained computation of nullifiers can be performed only on the ConfirmedNote type). If you were calling this from a constrained function, consider using compute_confirmed_note_hash_for_nullification instead. Unconstrained usage is safe.
[Aztec.nr] Changes to standard note hash computation
Note hashes used to be computed with the storage slot being the last value of the preimage, it is now the first. This is to make it easier to ensure all note hashes have proper domain separation.
This change requires no input from your side unless you were testing or relying on hardcoded note hashes.
4.1.3
[Aztec.js] TxReceipt now includes epochNumber
TxReceipt now includes an epochNumber field that indicates which epoch the transaction was included in.
[Aztec.js] computeL2ToL1MembershipWitness signature changed
The function signature has changed to resolve the epoch internally from a transaction hash, rather than requiring the caller to pass the epoch number.
Migration:
- const witness = await computeL2ToL1MembershipWitness(aztecNode, epochNumber, messageHash);
- // epoch was passed in by the caller
+ const witness = await computeL2ToL1MembershipWitness(aztecNode, messageHash, txHash);
+ // epoch is now available on the returned witness
+ const epoch = witness.epochNumber;
The return type L2ToL1MembershipWitness now includes epochNumber. An optional messageIndexInTx parameter can be passed as the fourth argument to disambiguate when a transaction emits multiple identical L2-to-L1 messages.
Impact: All call sites that compute L2-to-L1 membership witnesses must update to the new argument order and extract epochNumber from the result instead of passing it in.
[Aztec.js] getPublicEvents now returns an object instead of an array
getPublicEvents now returns a GetPublicEventsResult<T> object with events and maxLogsHit fields instead of a plain array. This enables pagination through large result sets using the new afterLog filter option.
- const events = await getPublicEvents<MyEvent>(node, MyContract.events.MyEvent, filter);
+ const { events } = await getPublicEvents<MyEvent>(node, MyContract.events.MyEvent, filter);
The maxLogsHit flag indicates whether the log limit was reached, meaning more results may be available. You can use afterLog in the filter to fetch the next page.
[Aztec.nr] Removed get_random_bytes
The get_random_bytes unconstrained function has been removed from aztec::utils::random. If you were using it, you can replace it with direct calls to the random oracle from aztec::oracle::random and convert to bytes yourself.
4.1.0-rc.2
[Aztec.js] simulate(), send(), and deploy return types changed to always return objects
All SDK interaction methods now return structured objects that include offchain output alongside the primary result. This affects .simulate(), .send(), deploy .send(), and Wallet.sendTx().
Impact: Every call site that uses .simulate(), .send(), or deploy must destructure the result. This is a mechanical transformation. Custom wallet implementations must update sendTx() to return the new object shapes, using extractOffchainOutput to decode offchain messages from raw effects.
The offchain output includes two fields:
offchainEffects— raw offchain effects emitted during execution, other thanoffchainMessagesoffchainMessages— decoded messages intended for specific recipients
We are making this change now so in the future we can add more fields to the responses of this APIs without breaking backwards compatibility, so this won't ever happen again.
simulate() — always returns { result, offchainEffects, offchainMessages } object:
- const value = await contract.methods.foo(args).simulate({ from: sender });
+ const { result: value } = await contract.methods.foo(args).simulate({ from: sender });
When using includeMetadata or fee.estimateGas, stats and estimatedGas are also available as optional fields on the same object:
- const { stats, estimatedGas } = await contract.methods.foo(args).simulate({
+ const sim = await contract.methods.foo(args).simulate({
from: sender,
includeMetadata: true,
});
+ const stats = sim.stats!;
+ const estimatedGas = sim.estimatedGas!;
SimulationReturn is no longer a generic conditional type — it's a single flat type with optional stats and estimatedGas fields.
send() — returns { receipt, offchainEffects, offchainMessages } object:
- const receipt = await contract.methods.foo(args).send({ from: sender });
+ const { receipt } = await contract.methods.foo(args).send({ from: sender });
When using NO_WAIT, returns { txHash, offchainEffects, offchainMessages } instead of a bare TxHash:
- const txHash = await contract.methods.foo(args).send({ from: sender, wait: NO_WAIT });
+ const { txHash } = await contract.methods.foo(args).send({ from: sender, wait: NO_WAIT });
Offchain messages emitted by the transaction are available on the result:
const { receipt, offchainMessages } = await contract.methods
.foo(args)
.send({ from: sender });
for (const msg of offchainMessages) {
console.log(
`Message for ${msg.recipient} from contract ${msg.contractAddress}:`,
msg.payload,
);
}
Deploy — returns { contract, receipt, offchainEffects, offchainMessages } object:
- const myContract = await MyContract.deploy(wallet, ...args).send({ from: sender });
+ const { contract: myContract } = await MyContract.deploy(wallet, ...args).send({ from: sender });
The deploy receipt is also available via receipt if needed (e.g. for receipt.txHash or receipt.transactionFee).
Custom wallet implementations — sendTx() must return objects:
If you implement the Wallet interface (or extend BaseWallet), the sendTx() method must now return objects that include offchain output. Use extractOffchainOutput to split raw effects into decoded messages and remaining effects:
+ import { extractOffchainOutput } from '@aztec/aztec.js/contracts';
async sendTx(executionPayload, opts) {
const provenTx = await this.pxe.proveTx(...);
+ const offchainOutput = extractOffchainOutput(provenTx.getOffchainEffects());
const tx = await provenTx.toTx();
const txHash = tx.getTxHash();
await this.aztecNode.sendTx(tx);
if (opts.wait === NO_WAIT) {
- return txHash;
+ return { txHash, ...offchainOutput };
}
const receipt = await waitForTx(this.aztecNode, txHash, opts.wait);
- return receipt;
+ return { receipt, ...offchainOutput };
}
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.
Noir developers (TXE)
TXE now enforces scope isolation, matching PXE behavior. During private execution, only the caller's keys and notes are accessible. If a Noir test accesses private state of an address other than from, it will fail. When from is the zero address, scopes are empty (deny-all).
If your TXE tests fail with key or note access errors, ensure the test is calling from the correct address, or restructure the test to match the expected access pattern.
Aztec.js developers (PXE/Wallet)
The wallet now passes scopes to PXE, and only the from address is in scope by default. Auto-expansion of scopes for nested calls to registered accounts has been removed. A new additionalScopes option is available on send(), simulate(), and deploy() for cases where private execution needs access to another address's keys or notes.
When do you need additionalScopes?
-
Deploying contracts whose constructor initializes private storage (e.g., account contracts, or any contract using
SinglePrivateImmutable/SinglePrivateMutablein the constructor). The contract's own address must be in scope so its nullifier key is accessible during initialization. -
Operations that access another contract's private state (e.g., withdrawing from an escrow contract that nullifies the contract's own token notes).
**Example: deploying a contract with private storage (e.g., `PrivateToken`)**
```diff
const tokenDeployment = PrivateTokenContract.deployWithPublicKeys(
tokenPublicKeys, wallet, initialBalance, sender,
);
const tokenInstance = await tokenDeployment.getInstance();
await wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey);
const token = await tokenDeployment.send({
from: sender,
+ additionalScopes: [tokenInstance.address],
});
Example: withdrawing from an escrow contract
await escrowContract.methods
.withdraw(token.address, amount, recipient)
- .send({ from: owner });
+ .send({ from: owner, additionalScopes: [escrowContract.address] });
simulateUtility renamed to executeUtility
The simulateUtility method and related types have been renamed to executeUtility across the entire stack to better reflect that utility functions are executed, not simulated.
TypeScript:
- import { SimulateUtilityOptions, UtilitySimulationResult } from '@aztec/aztec.js';
+ import { ExecuteUtilityOptions, UtilityExecutionResult } from '@aztec/aztec.js';
- const result: UtilitySimulationResult = await wallet.simulateUtility(functionCall, opts);
+ const result: UtilityExecutionResult = await wallet.executeUtility(functionCall, opts);
Noir (test environment):
- let result = env.simulate_utility(my_contract_address, selector);
+ let result = env.execute_utility(my_contract_address, selector);
4.0.0-devnet.2-patch.0
[Protocol] include_by_timestamp renamed to expiration_timestamp
The include_by_timestamp field has been renamed to expiration_timestamp across the protocol to better convey its meaning.
Noir:
- context.set_tx_include_by_timestamp(123456789);
+ context.set_expiration_timestamp(123456789);
[CLI] Dockerless CLI Installation
The Aztec CLI is now installed without Docker. The installation command has changed:
Old installation (deprecated):
bash -i <(curl -sL https://install.aztec.network)
aztec-up <version>
New installation:
VERSION=<version> bash -i <(curl -sL https://install.aztec.network/<version>)
For example, to install version 4.2.0:
VERSION=4.2.0 bash -i <(curl -sL https://install.aztec.network/4.2.0)
Key changes:
- Docker is no longer required to run the Aztec CLI tools
- The
VERSIONenvironment variable must be set in the installation command - The version must also be included in the URL path
aztec-up is now a version manager:
After installation, aztec-up functions as a version manager with the following commands:
| Command | Description |
|---|---|
aztec-up install <version> | Install a specific version and switch to it |
aztec-up use <version> | Switch to an already installed version |
aztec-up list | List all installed versions |
aztec-up self-update | Update aztec-up itself |
@aztec/test-wallet replaced by @aztec/wallets
The @aztec/test-wallet package has been removed. Use @aztec/wallets instead, which provides EmbeddedWallet with a static create() factory:
- import { TestWallet, registerInitialLocalNetworkAccountsInWallet } from '@aztec/test-wallet/server';
+ import { EmbeddedWallet } from '@aztec/wallets/embedded';
+ import { registerInitialLocalNetworkAccountsInWallet } from '@aztec/wallets/testing';
- const wallet = await TestWallet.create(node);
+ const wallet = await EmbeddedWallet.create(node);
For browser environments, the same import resolves to a browser-specific implementation automatically via conditional exports:X
The EmbeddedWallet.create() factory accepts an optional second argument for logger injection and ephemeral storage:
const wallet = await EmbeddedWallet.create(node, {
logger: myLogger, // custom logger; child loggers derived via createChild()
ephemeral: true, // use in-memory stores (no persistence)
});
[Aztec.nr] debug_log module renamed to logging
The debug_log module has been renamed to logging to avoid naming collisions with per-level logging functions that were introduced in this PR (warn_log, info_log, debug_log... and the "format" versions warn_log_format, debug_log_format). Update all import paths accordingly:
- use aztec::oracle::debug_log::debug_log;
- use aztec::oracle::debug_log::debug_log_format;
+ use aztec::oracle::logging::debug_log;
+ use aztec::oracle::logging::debug_log_format;
For inline paths:
- aztec::oracle::debug_log::debug_log_format("msg: {}", [value]);
+ aztec::oracle::logging::debug_log_format("msg: {}", [value]);
The function names themselves (debug_log, debug_log_format, debug_log_with_level, debug_log_format_with_level) are unchanged.
Additionally, debug_log_format_slice has been removed. Use debug_log_format instead, which accepts a fixed-size array of fields:
- debug_log_format_slice("values: {}", &[value1, value2]);
+ debug_log_format("values: {}", [value1, value2]);
This has been done as usage of Noir slices is discouraged and the function was unused in the aztec codebase.
[AztecNode] Sentinel validator status values renamed
The ValidatorStatusInSlot values returned by getValidatorsStats and getValidatorStats have been updated to reflect the multi-block-per-slot model, where blocks and checkpoints are distinct concepts:
- 'block-mined'
+ 'checkpoint-mined'
- 'block-proposed'
+ 'checkpoint-proposed'
- 'block-missed'
+ 'checkpoint-missed' // blocks were proposed but checkpoint was not attested
+ 'blocks-missed' // no block proposals were sent at all
The attestation-sent and attestation-missed values are unchanged but now explicitly refer to checkpoint attestations.
The ValidatorStatusType used for categorizing statuses has also changed from 'block' | 'attestation' to 'proposer' | 'attestation'.
[aztec.js] getDecodedPublicEvents renamed to getPublicEvents with new signature
The getDecodedPublicEvents function has been renamed to getPublicEvents and now uses a filter object instead of positional parameters:
- import { getDecodedPublicEvents } from '@aztec/aztec.js/events';
+ import { getPublicEvents } from '@aztec/aztec.js/events';
- const events = await getDecodedPublicEvents(node, eventMetadata, fromBlock, limit);
+ const events = await getPublicEvents(node, eventMetadata, {
+ fromBlock,
+ toBlock,
+ contractAddress, // optional
+ txHash, // optional
+ });
The new function returns richer metadata including contractAddress, txHash, l2BlockNumber, and l2BlockHash for each event:
import { getPublicEvents } from "@aztec/aztec.js/events";
import { MyContract } from "./artifacts/MyContract.js";
// Query events from a contract
const events = await getPublicEvents<{ amount: bigint; sender: AztecAddress }>(
aztecNode,
MyContract.events.Transfer,
{ contractAddress: myContractAddress, fromBlock: BlockNumber(1) },
);
// Each event includes decoded data and metadata
for (const { event, metadata } of events) {
console.log(`Transfer of ${event.amount} from ${event.sender}`);
console.log(` Block: ${metadata.l2BlockNumber}, Tx: ${metadata.txHash}`);
console.log(` Contract: ${metadata.contractAddress}`);
}
[Aztec.nr] nophasecheck renamed as allow_phase_change
[AztecNode] Removed sibling path RPC methods
The following methods have been removed from the AztecNode interface:
getNullifierSiblingPathgetNoteHashSiblingPathgetArchiveSiblingPathgetPublicDataSiblingPath
These methods were not used by PXE and returned a subset of the information already available through the corresponding membership witness methods:
| Removed Method | Use Instead |
|---|---|
getNullifierSiblingPath | getNullifierMembershipWitness |
getNoteHashSiblingPath | getNoteHashMembershipWitness |
getArchiveSiblingPath | getBlockHashMembershipWitness |
getPublicDataSiblingPath | getPublicDataWitness |
The membership witness methods return both the sibling path and additional context (leaf index, preimage data) needed for proofs.
[Protocol] "Nullifier secret key" renamed to "nullifier hiding key" (nsk → nhk)
The nullifier secret key (nsk_m / nsk_app) has been renamed to nullifier hiding key (nhk_m / nhk_app). This is a protocol-breaking change: the domain separator string changes from "az_nsk_m" to "az_nhk_m", producing a different constant value.
Noir changes:
- context.request_nsk_app(npk_m_hash)
+ context.request_nhk_app(npk_m_hash)
- get_nsk_app(npk_m_hash)
+ get_nhk_app(npk_m_hash)
TypeScript changes:
- import { computeAppNullifierSecretKey, deriveMasterNullifierSecretKey } from '@aztec/stdlib/keys';
+ import { computeAppNullifierHidingKey, deriveMasterNullifierHidingKey } from '@aztec/stdlib/keys';
- const masterNullifierSecretKey = deriveMasterNullifierSecretKey(secret);
+ const masterNullifierHidingKey = deriveMasterNullifierHidingKey(secret);
- const nskApp = await computeAppNullifierSecretKey(masterNullifierSecretKey, contractAddress);
+ const nhkApp = await computeAppNullifierHidingKey(masterNullifierHidingKey, contractAddress);
The GeneratorIndex.NSK_M enum member is now GeneratorIndex.NHK_M.
[AztecNode/Aztec.nr] getArchiveMembershipWitness renamed to getBlockHashMembershipWitness
The getArchiveMembershipWitness method has been renamed to getBlockHashMembershipWitness to better reflect its purpose. Block hashes are the leaves of the archive tree - each time a new block is added to the chain, its block hash is appended as a new leaf. This rename clarifies that the method finds a membership witness for a block hash in the archive tree.
TypeScript (AztecNode interface):
- const witness = await aztecNode.getArchiveMembershipWitness(blockNumber, archiveLeaf);
+ const witness = await aztecNode.getBlockHashMembershipWitness(blockNumber, blockHash);
The second parameter type has also changed from Fr to BlockHash.
Noir (aztec-nr):
- use dep::aztec::oracle::get_membership_witness::get_archive_membership_witness;
+ use dep::aztec::oracle::get_membership_witness::get_block_hash_membership_witness;
- let witness = get_archive_membership_witness(block_header, leaf_value);
+ let witness = get_block_hash_membership_witness(anchor_block_header, block_hash);
[Aztec.nr] protocol_types renamed to protocol
The protocol_types re-export from the aztec crate has been renamed to protocol. Update all imports accordingly:
- use dep::aztec::protocol_types::address::AztecAddress;
+ use dep::aztec::protocol::address::AztecAddress;
Protocol contract interface separate from protocol contracts
We've stripped protocol contract of aztec-nr macros in order for auditors to not need to audit them (protocol contracts are to be audited during the protocol circuits audit).
This results in the nice Noir interface no longer being generated.
For context, this is the interface I am talking about:
let update_delay = self.view(MyContract::at(my_contract_address).my_fn());
where the macros generate the MyContract struct.
For this reason we've created place holder protocol contracts in noir-projects/noir-contracts/contracts/protocol_interface that still have these macros applied and hence you can use them to get the interface.
On your side all you need to do is update the dependency in Nargo.toml:
-instance_contract = { path = "../../protocol/contract_instance_registry" }
+instance_contract = { path = "../../protocol_interface/contract_instance_registry_interface" }
[aztec-nr] History module refactored to use standalone functions
The aztec::history module has been refactored to use standalone functions instead of traits. This changes the calling convention from method syntax to function syntax.
- use dep::aztec::history::note_inclusion::ProveNoteInclusion;
+ use dep::aztec::history::note::assert_note_existed_by;
let block_header = context.get_anchor_block_header();
- let confirmed_note = block_header.prove_note_inclusion(hinted_note);
+ let confirmed_note = assert_note_existed_by(block_header, hinted_note);
Function name and module mapping:
| Old (trait method) | New (standalone function) |
|---|---|
history::note_inclusion::prove_note_inclusion | history::note::assert_note_existed_by |
history::note_validity::prove_note_validity | history::note::assert_note_was_valid_by |
history::nullifier_inclusion::prove_nullifier_inclusion | history::nullifier::assert_nullifier_existed_by |
history::nullifier_inclusion::prove_note_is_nullified | history::note::assert_note_was_nullified_by |
history::nullifier_non_inclusion::prove_nullifier_non_inclusion | history::nullifier::assert_nullifier_did_not_exist_by |
history::nullifier_non_inclusion::prove_note_not_nullified | history::note::assert_note_was_not_nullified_by |
history::contract_inclusion::prove_contract_deployment | history::deployment::assert_contract_bytecode_was_published_by |
history::contract_inclusion::prove_contract_non_deployment | history::deployment::assert_contract_bytecode_was_not_published_by |
history::contract_inclusion::prove_contract_initialization | history::deployment::assert_contract_was_initialized_by |
history::contract_inclusion::prove_contract_non_initialization | history::deployment::assert_contract_was_not_initialized_by |
history::public_storage::public_storage_historical_read | history::storage::public_storage_historical_read |
[Aztec.js] Transaction sending API redesign
The old chained .send().wait() pattern has been replaced with a single .send(options) call that handles both sending and waiting.
+ import { Contract, NO_WAIT } from '@aztec/aztec.js/contracts';
- const receipt = await contract.methods.transfer(recipient, amount).send().wait();
// Send now waits by default
+ const receipt = await contract.methods.transfer(recipient, amount).send({ from: sender });
// getTxHash() would confusingly send the transaction too
- const txHash = await contract.methods.transfer(recipient, amount).send().getTxHash();
// NO_WAIT to send the transaction and return TxHash immediately
+ const txHash = await contract.methods.transfer(recipient, amount).send({
+ from: sender,
+ wait: NO_WAIT
+ });
Deployment changes
The old .send().deployed() method has been removed. Deployments now return the contract instance by default, or you can request the full receipt with returnReceipt: true:
- const contract = await MyContract.deploy(wallet, ...args).send().deployed();
- const { contract, instance } = await MyContract.deploy(wallet, ...args).send().wait();
+ const contract = await MyContract.deploy(wallet, ...args).send({ from: deployer });
+ const { contract, instance } = await MyContract.deploy(wallet, ...args).send({
+ from: deployer,
+ wait: { returnReceipt: true },
+ });
Breaking changes to Wallet interface
getTxReceipt() has been removed from the interface.
sendTx method signature has changed to support the new wait behavior:
- sendTx(payload: ExecutionPayload, options: SendOptions): Promise<TxReceipt>
+ sendTx<W extends InteractionWaitOptions = undefined>(
+ payload: ExecutionPayload,
+ options: SendOptions<W>
+ ): Promise<SendReturn<W>>
Manual waiting with waitForTx
When using NO_WAIT to send transactions, you can manually wait for confirmation using the waitForTx utility:
import { waitForTx } from "@aztec/aztec.js/node";
const txHash = await contract.methods.transfer(recipient, amount).send({
from: sender,
wait: NO_WAIT,
});
const receipt = await waitForTx(node, txHash, {
timeout: 60000, // Optional: timeout in ms
interval: 1000, // Optional: polling interval in ms
dontThrowOnRevert: true, // Optional: return receipt even if tx reverted
});
[aztec-nr] Removal of intermediate modules
Lots of unnecessary modules have been removed from the API, making imports shorter. These are the modules that contain just a single struct, in which the module has the same name as the struct.
- use aztec::state_vars::private_mutable::PrivateMutable;
+ use aztec::state_vars::PrivateMutable;
Affected structs include all state variables, notes, contexts, messages, etc.
[L1 Contracts] Fee asset pricing direction inverted
The fee model now uses ethPerFeeAsset instead of the previous feeAssetPerEth. This change inverts how the exchange rate is represented: values now express how much ETH one fee asset (AZTEC) is worth, with 1e12 precision.
Key changes:
FeeHeader.feeAssetPerEth→FeeHeader.ethPerFeeAssetRollupConfigInputnow requiresinitialEthPerFeeAssetparameter at deployment- Default value:
1e7(0.00001 ETH per AZTEC) - Valid range:
100(1e-10 ETH/AZTEC) to1e11(0.1 ETH/AZTEC)
New environment variable for node operators:
AZTEC_INITIAL_ETH_PER_FEE_ASSET- Sets the initial ETH per fee asset price with 1e12 precision
[L1 Contracts] Fee asset price modifier now in basis points
The OracleInput.feeAssetPriceModifier field now expects values in basis points (BPS) instead of the previous representation. The modifier is applied as a percentage change to the ETH/AZTEC price each checkpoint.
Key changes:
- Valid range:
-100to+100BPS (±1% max change per checkpoint) - A value of
+100increases the price by 1%,-100decreases by 1% - Validated by
MAX_FEE_ASSET_PRICE_MODIFIER_BPS = 100
[Aztec.js] Wallet batching now supports all methods
The BatchedMethod type is now a discriminated union that ensures type safety: the args must match the specific method name. This prevents runtime errors from mismatched arguments.
- // Before: Only 5 methods could be batched
- const results = await wallet.batch([
- { name: "registerSender", args: [address, "alias"] },
- { name: "sendTx", args: [payload, options] },
- ]);
+ // After: All methods can be batched
+ const results = await wallet.batch([
+ { name: "getChainInfo", args: [] },
+ { name: "getContractMetadata", args: [contractAddress] },
+ { name: "registerSender", args: [address, "alias"] },
+ { name: "simulateTx", args: [payload, options] },
+ { name: "sendTx", args: [payload, options] },
+ ]);
[Aztec.js] Refactored getContractMetadata and getContractClassMetadata in Wallet
The contract metadata methods in the Wallet interface have been refactored to provide more granular information and avoid expensive round-trips.
ContractMetadata:
{
- contractInstance?: ContractInstanceWithAddress,
+ instance?: ContractInstanceWithAddress; // Instance registered in the Wallet, if any
isContractInitialized: boolean; // Is the init nullifier onchain? (already there)
isContractPublished: boolean; // Has the contract been published? (already there)
+ isContractUpdated: boolean; // Has the contract been updated?
+ updatedContractClassId?: Fr; // If updated, the new class ID
}
ContractClassMetadata:
This method loses the ability to request the contract artifact via the includeArtifact flag
{
- contractClass?: ContractClassWithId;
- artifact?: ContractArtifact;
isContractClassPubliclyRegistered: boolean; // Is the class registered onchain?
+ isArtifactRegistered: boolean; // Does the Wallet know about this artifact?
}
- Removes expensive artifact/class transfers between wallet and app
- Separates PXE storage info (
instance,isArtifactRegistered) from public chain info (isContractPublished,isContractClassPubliclyRegistered) - Makes it easier to determine if actions like
registerContractare needed
[Aztec.js] Removed UnsafeContract and protocol contract helper functions
The UnsafeContract class and async helper functions (getFeeJuice, getClassRegistryContract, getInstanceRegistryContract) have been removed. Protocol contracts are now accessed via auto-generated type-safe wrappers with only the ABI (no bytecode). Since PXE always has protocol contract artifacts available, importing and using these contracts from aztec.js is very lightweight and follows the same pattern as regular user contracts.
Migration:
- import { getFeeJuice, getClassRegistryContract, getInstanceRegistryContract } from '@aztec/aztec.js/contracts';
+ import { FeeJuiceContract, ContractClassRegistryContract, ContractInstanceRegistryContract } from '@aztec/aztec.js/protocol';
- const feeJuice = await getFeeJuice(wallet);
+ const feeJuice = FeeJuiceContract.at(wallet);
await feeJuice.methods.check_balance(feeLimit).send().wait();
- const classRegistry = await getClassRegistryContract(wallet);
+ const classRegistry = ContractClassRegistryContract.at(wallet);
await classRegistry.methods.publish(...).send().wait();
- const instanceRegistry = await getInstanceRegistryContract(wallet);
+ const instanceRegistry = ContractInstanceRegistryContract.at(wallet);
await instanceRegistry.methods.publish_for_public_execution(...).send().wait();
Note: The higher-level utilities like publishInstance, publishContractClass, and broadcastPrivateFunction from @aztec/aztec.js/deployment are still available and unchanged. These utilities use the new wrappers internally.
[Aztec.nr] Renamed Router contract
Router contract has been renamed as PublicChecks contract.
The name of the contract became stale as its use changed from routing public calls through it to simply having public functions that can be called by anyone.
Having these "standard checks" on one contract results in a potentially large privacy set for apps that use it.
[Aztec Node] getBlockByHash and getBlockHeaderByHash removed
The getBlockByHash and getBlockHeaderByHash methods have been removed. Use getBlock and getBlockHeader with a block hash instead.
Migration:
- const block = await node.getBlockByHash(blockHash);
+ const block = await node.getBlock(blockHash);
- const header = await node.getBlockHeaderByHash(blockHash);
+ const header = await node.getBlockHeader(blockHash);
[Aztec.nr] Oracle functions now take BlockHeader instead of block number
The low-level oracle functions for fetching membership witnesses and storage now take a BlockHeader instead of a block_number: u32. This change improves type safety and ensures the correct block state is queried.
Affected functions:
get_note_hash_membership_witness(block_header, leaf_value)- was(block_number, leaf_value)get_archive_membership_witness(block_header, leaf_value)- was(block_number, leaf_value)get_nullifier_membership_witness(block_header, nullifier)- was(block_number, nullifier)get_low_nullifier_membership_witness(block_header, nullifier)- was(block_number, nullifier)get_public_data_witness(block_header, public_data_tree_index)- was(block_number, public_data_tree_index)storage_read(block_header, address, storage_slot)- was(address, storage_slot, block_number)
Migration:
If you were calling these oracle functions directly (which is uncommon), update your code to pass a BlockHeader instead of a block number:
- let witness = get_note_hash_membership_witness(self.global_variables.block_number, note_hash);
+ let witness = get_note_hash_membership_witness(self, note_hash);
- let witness = get_nullifier_membership_witness(block_number, nullifier);
+ let witness = get_nullifier_membership_witness(block_header, nullifier);
- let value: T = storage_read(address, slot, block_number);
+ let value: T = storage_read(block_header, address, slot);
Note: The high-level history proof functions on BlockHeader (such as prove_note_inclusion, prove_nullifier_inclusion, etc.) are not affected by this change. They continue to work the same way.
[Toolchain] Node.js upgraded to v24
Node.js minimum version changed from v22 to v24.12.0.
[L1 Contracts] Renamed base fee to min fee
The L1 rollup contract functions and types related to fee calculation have been renamed from "base fee" to "min fee" to better reflect their purpose.
Renamed functions:
getManaBaseFeeAt→getManaMinFeeAtgetManaBaseFeeComponentsAt→getManaMinFeeComponentsAt
Renamed types:
ManaBaseFeeComponents→ManaMinFeeComponents
Renamed errors:
Rollup__InvalidManaBaseFee→Rollup__InvalidManaMinFee
Migration:
- uint256 fee = rollup.getManaBaseFeeAt(timestamp, true);
+ uint256 fee = rollup.getManaMinFeeAt(timestamp, true);
- ManaBaseFeeComponents memory components = rollup.getManaBaseFeeComponentsAt(timestamp, true);
+ ManaMinFeeComponents memory components = rollup.getManaMinFeeComponentsAt(timestamp, true);
[Aztec.js] Renamed base fee to min fee
The Aztec Node API method for getting current fees has been renamed:
getCurrentBaseFees→getCurrentMinFees
Migration:
- const fees = await node.getCurrentBaseFees();
+ const fees = await node.getCurrentMinFees();
[Aztec.nr] Renamed fee context methods
The context methods for accessing fee information have been renamed:
context.base_fee_per_l2_gas()→context.min_fee_per_l2_gas()context.base_fee_per_da_gas()→context.min_fee_per_da_gas()
Migration:
- let l2_fee = context.base_fee_per_l2_gas();
- let da_fee = context.base_fee_per_da_gas();
+ let l2_fee = context.min_fee_per_l2_gas();
+ let da_fee = context.min_fee_per_da_gas();
[Aztec.nr] Cleaning up message sender functions
There has been a design decision made to have low-level API exposed on self.context and a nicer higher-level API exposed directly on self.
Currently the msg_sender function on self was a copy of that same function on self.context.
The msg_sender function on self got modified to return the message sender address directly instead of having it be wrapped in an Option<...>.
In case the underlying message sender is none the function panics.
You need to update your code to no longer trigger the unwrap on the return value:
- let message_sender: AztecAddress = self.msg_sender().unwrap();
+ let message_sender: AztecAddress = self.msg_sender();
If you want to handle the null case use the lower level API of context:
- let maybe_message_sender: Option<AztecAddress> = self.msg_sender();
+ let maybe_message_sender: Option<AztecAddress> = self.context.maybe_msg_sender();
The self.context.msg_sender_unsafe method has been dropped as its use can be replaced with the standard self.context.maybe_msg_sender function.
[Aztec.nr] Renamed message delivery options
The following terms have been renamed:
MessageDelivery::UNCONSTRAINED_OFFCHAIN->MessageDelivery::OFFCHAINMessageDelivery::UNCONSTRAINED_ONCHAIN->MessageDelivery::ONCHAIN_UNCONSTRAINEDMessageDelivery::CONSTRAINED_ONCHAIN->MessageDelivery::ONCHAIN_CONSTRAINED
We believe these names will better convey the meaning of the concepts.
[Aztec Node] changes to getLogsByTags endpoint
getLogsByTags endpoint has been optimized for our new log sync algorithm and these are the changes:
- The
logsPerTagpagination argument has been removed. Pagination was unnecessary here, since multiple logs per tag typically only occur if several devices are sending logs from the same sender to a recipient, which is unlikely to generate enough logs to require pagination. - The structure of
TxScopedL2Loghas been revised to meet the requirements of our new log sync algorithm. - The endpoint has been separated into two versions:
getPrivateLogsByTagsandgetPublicLogsByTagsFromContract. This change was made because it was never desirable in PXE to mix public and private logs. The public version requires both aTagand a contract address as input. In contrast to the private version—which usesSiloedTag(a tag that hashes the raw tag with the emitting contract's address)—the public version uses the rawTagtype, since kernels do not hash the tag with the contract address for public logs.
[AVM] Gas cost multipliers for public execution to reach simulation/proving parity
Gas costs for several AVM opcodes have been adjusted with multipliers to better align public simulation costs with actual proving costs.
| Opcode | Multiplier | Previous Cost | New Cost |
|---|---|---|---|
| FDIV | 25x | 9 | 225 |
| SLOAD | 10x | 129 | 1,290 |
| SSTORE | 20x | 1,657 | 33,140 |
| NOTEHASHEXISTS | 4x | 126 | 504 |
| EMITNOTEHASH | 15x | 1,285 | 19,275 |
| NULLIFIEREXISTS | 7x | 132 | 924 |
| EMITNULLIFIER | 20x | 1,540 | 30,800 |
| L1TOL2MSGEXISTS | 5x | 108 | 540 |
| SENDL2TOL1MSG | 2x | 209 | 418 |
| CALL | 3x | 3,312 | 9,936 |
| STATICCALL | 3x | 3,312 | 9,936 |
| GETCONTRACTINSTANCE | 4x | 1,527 | 6,108 |
| POSEIDON2 | 15x | 24 | 360 |
| ECADD | 10x | 27 | 270 |
Impact: Contracts with public bytecode performing any of these operations will see increased gas consumption.
[PXE] deprecated getNotes
This function serves only for debugging purposes so we are taking it out of the main PXE API. If you still need to consume it, you can
do so through the new debug sub-module.
- this.pxe.getNotes(filter);
+ this.pxe.debug.getNotes(filter);
3.0.0-devnet.20251212
[Aztec node, archiver] Deprecated getPrivateLogs
Aztec node no longer offers a getPrivateLogs method. If you need to process the logs of a block, you can instead use getBlock and call getPrivateLogs on an L2BlockNew instance. See the diff below for before/after equivalent code samples.
- const logs = await aztecNode.getPrivateLogs(blockNumber, 1);
+ const logs = (await aztecNode.getBlock(blockNumber))?.toL2Block().getPrivateLogs();
[Aztec.nr] Private event emission API changes
Private events are still emitted via the emit function, but this now returns an EventMessage type that must have deliver_to called on it in order to deliver the event message to the intended recipients. This allows for multiple recipients to receive the same event.
- self.emit(event, recipient, delivery_method)
+ self.emit(event).delivery(recipient, delivery_method)
[Aztec.nr] History proof functions no longer require storage_slot parameter
The HintedNote struct now includes a storage_slot field, making it self-contained for proving note inclusion and validity. As a result, the history proof functions in the aztec::history module no longer require a separate storage_slot parameter.
Affected functions:
BlockHeader::prove_note_inclusion- removedstorage_slot: FieldparameterBlockHeader::prove_note_validity- removedstorage_slot: FieldparameterBlockHeader::prove_note_is_nullified- removedstorage_slot: FieldparameterBlockHeader::prove_note_not_nullified- removedstorage_slot: Fieldparameter
Migration:
The storage_slot is now read from hinted_note.storage_slot internally. Simply remove the storage_slot argument from all calls to these functions:
let header = context.get_anchor_block_header();
- header.prove_note_inclusion(hinted_note, storage_slot);
+ header.prove_note_inclusion(hinted_note);
let header = context.get_anchor_block_header();
- header.prove_note_validity(hinted_note, storage_slot, context);
+ header.prove_note_validity(hinted_note, context);
let header = context.get_anchor_block_header();
- header.prove_note_is_nullified(hinted_note, storage_slot, context);
+ header.prove_note_is_nullified(hinted_note, context);
let header = context.get_anchor_block_header();
- header.prove_note_not_nullified(hinted_note, storage_slot, context);
+ header.prove_note_not_nullified(hinted_note, context);
[Aztec.nr] Note fields are now public
All note struct fields are now public, and the new() constructor methods and getter methods have been removed. Notes should be instantiated using struct literal syntax, and fields should be accessed directly.
The motivation for this change has been enshrining of randomness which lead to the new method being unnecessary boilerplate.
Affected notes:
UintNote-valueis now public,new()andget_value()removedAddressNote-addressis now public,new()andget_address()removedFieldNote-valueis now public,new()andvalue()removed
Migration:
- let note = UintNote::new(100);
+ let note = UintNote { value: 100 };
- let value = note.get_value();
+ let value = note.value;
- let address_note = AddressNote::new(owner);
+ let address_note = AddressNote { address: owner };
- let address = address_note.get_address();
+ let address = address_note.address;
- let field_note = FieldNote::new(42);
+ let field_note = FieldNote { value: 42 };
- let value = field_note.value();
+ let value = field_note.value;
[Aztec.nr] emit renamed to deliver
Private state variable functions that created notes and returned their messages no longer return a NoteEmission but instead a NoteMessage. These messages are delivered to their owner via deliver instead of emit. The verb 'emit' remains for things like emitting events.
- self.storage.balances.at(owner).add(5).emit(owner);
+ self.storage.balances.at(owner).add(5).deliver();
To deliver a message to a different recipient, use deliver_to:
- self.storage.balances.at(owner).add(5).emit(other);
+ self.storage.balances.at(owner).add(5).deliver_to(other);
[Aztec.nr] ValueNote renamed to FieldNote and value-note crate renamed to field-note
The ValueNote struct has been renamed to FieldNote to better reflect that it stores a Field value. The crate has also been renamed from value-note to field-note.
Migration:
- Update your
Nargo.tomldependencies:value_note = { path = "..." }→field_note = { path = "..." } - Update imports:
use value_note::value_note::ValueNote→use field_note::field_note::FieldNote - Update type references:
ValueNote→FieldNote - Update generic parameters:
PrivateSet<ValueNote, ...>→PrivateSet<FieldNote, ...>
[Aztec.nr] New balance-set library for managing token balances
A new balance-set library has been created that provides BalanceSet<Context> for managing u128 token balances with UintNote. This consolidates balance management functionality that was previously duplicated across contracts.
Features:
add(amount: u128)- Add to balancesub(amount: u128)- Subtract from balance (with change note)try_sub(amount: u128, max_notes: u32)- Attempt to subtract with configurable note limitbalance_of()- Get total balance (unconstrained)
Usage:
use balance_set::BalanceSet;
#[storage]
struct Storage<Context> {
balances: Owned<BalanceSet<Context>, Context>,
}
// In a private function:
self.storage.balances.at(owner).add(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN);
self.storage.balances.at(owner).sub(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN);
// In an unconstrained function:
let balance = self.storage.balances.at(owner).balance_of();
[Aztec.nr] EasyPrivateUint deprecated and removed
The EasyPrivateUint type and easy-private-state crate have been deprecated and removed. Use BalanceSet from the balance-set crate instead.
Migration:
- Remove
easy_private_statedependency fromNargo.toml - Add
balance_set = { path = "../../../../aztec-nr/balance-set" }toNargo.toml - Update storage:
EasyPrivateUint<Context>→Owned<BalanceSet<Context>, Context> - Update method calls:
add(amount, owner)→at(owner).add(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN)sub(amount, owner)→at(owner).sub(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN)get_value(owner)→at(owner).balance_of()(returnsu128instead ofField)
[Aztec.nr] balance_utils removed from value-note (now field-note)
The balance_utils module has been removed from the field-note crate (formerly value-note). If you need similar functionality, implement it locally in your contract or use BalanceSet for u128 balances.
[Aztec.nr] filter_notes_min_sum removed from value-note (now field-note)
The filter_notes_min_sum function has been removed from the field-note crate (formerly in value-note). If you need this functionality, copy it to your contract locally. This function was only used in specific test contracts and doesn't belong in the general-purpose note library.
[Aztec.nr] derive_ecdh_shared_secret_using_aztec_address removed
This function made it annoying to deal with invalid addresses in circuits. If you were using it, replace it with derive_ecdh_shared_secret instead:
-let shared_secret = derive_ecdh_shared_secret_using_aztec_address(secret, address).unwrap();
+let shared_secret = derive_ecdh_shared_secret(secret, address.to_address_point().unwrap().inner);
[Aztec.nr] Note owner is now enshrined
It turns out that in all cases a note always has a logical owner. For this reason we have decided to enshrine the concept of a note owner and you should drop the field from your note:
#[derive(Deserialize, Eq, Packable, Serialize)]
#[note]
pub struct ValueNote {
value: Field,
- owner: AztecAddress,
}
The owner being enshrined means that our API explicitly expects it on the input.
The NoteHash trait got modified as follows:
pub trait NoteHash {
fn compute_note_hash(
self,
+ owner: AztecAddress,
storage_slot: Field,
randomness: Field,
) -> Field;
fn compute_nullifier(
self,
context: &mut PrivateContext,
+ owner: AztecAddress,
note_hash_for_nullification: Field,
) -> Field;
unconstrained fn compute_nullifier_unconstrained(
self,
+ owner: AztecAddress,
note_hash_for_nullification: Field,
) -> Field;
}
Our low-level note utilities now also accept owner as a parameter:
pub fn create_note<Note>(
context: &mut PrivateContext,
+ owner: AztecAddress,
storage_slot: Field,
note: Note,
) -> NoteEmission<Note>
where
Note: NoteType + NoteHash + Packable,
{
...
}
Signature of some functions like destroy_note_unsafe is unchanged:
pub fn destroy_note_unsafe<Note>(
context: &mut PrivateContext,
hinted_note: HintedNote<Note>,
note_hash_read: NoteHashRead,
)
where
Note: NoteHash,
{
...
}
because HintedNote now contains owner.
PrivateImmutable, PrivateMutable and PrivateSet got modified to directly contain the owner instead of implicitly "containing it" by including it in the storage slot via a Map.
These state variables now implement a newly introduced OwnedStateVariable trait (see docs of OwnedStateVariable for explanation of what it is).
These changes make the state variables incompatible with Map and now instead these should be wrapped in new Owned state variable:
#[storage]
struct Storage<Context> {
- private_nfts: Map<AztecAddress, PrivateSet<NFTNote, Context>, Context>,
+ private_nfts: Owned<PrivateSet<NFTNote, Context>, Context>,
}
Note that even though the types of your state variables are changing from Map<AztecAddress, T, Context> to Owned<T, Context>, usage remains unchanged:
let nft_notes = self.storage.private_nfts.at(from).pop_notes(NoteGetterOptions::new().select(NFTNote::properties().token_id, Comparator.EQ, token_id).set_limit(1));
With this change the underlying notes will inherit the storage slot of the Owned state variable.
This is unlike Map where the nested state variable got the storage slot computed as hash([map_storage_slot, key]).
if you had PrivateImmutable or PrivateMutable defined out of a Map, e.g.:
#[storage]
struct Storage<Context> {
signing_public_key: PrivateImmutable<PublicKeyNote, Context>,
}
you were most likely dealing with some kind of admin flow where only the admin can modify the state variable.
Now, unfortunately, there is a bit of a regression and you will need to wrap the state variable in Owned and call at on the state var:
+ use aztec::state_vars::Owned;
#[storage]
struct Storage<Context> {
- signing_public_key: PrivateImmutable<PublicKeyNote, Context>,
+ signing_public_key: Owned<PrivateImmutable<PublicKeyNote, Context>, Context>,
}
#[external("private")]
fn my_external_function() {
- self.storage.signing_public_key.initialize(pub_key_note)
+ self.storage.signing_public_key.at(self.address).initialize(pub_key_note)
.emit(self.address, MessageDelivery.CONSTRAINED_ONCHAIN);
}
We are likely to come up with a concept of admin state variables in the future.
None of the reference notes now contain the owner so if you manually construct AddressNote, UintNote or ValueNote you need to update the call to new method:
- let note = UintNote::new(156, owner);
+ let note = UintNote::new(156);
[Aztec.nr] Note randomness is now handled internally
In order to prevent pre-image attacks, it is necessary to inject randomness to notes. Aztec.nr users were previously expected to add said randomness to their custom note types. From now on, Aztec.nr takes care of handling randomness as built-in note metadata, making it impossible to miss for library users. This change breaks backwards compatibility as we'll discuss below.
Changes to Aztec.nr note types
If you're using any of the following note types, please be aware that randomness no longer is an explicit attribute in them.
- ValueNote
- UintNote
- NFTNote
- AddressNote
Migrating your custom note types: refer to UintNote as an example of how to migrate
We show the changes to UintNote below since it serves as a good example of the adjustments you will need to make to your own custom note types, including those that need to support partial notes.
Remove randomness from note struct
pub struct UintNote {
/// The owner of the note, i.e. the account whose nullifier secret key is required to compute the nullifier.
owner: AztecAddress,
- /// Random value, protects against note hash preimage attacks.
- randomness: Field,
/// The number stored in the note.
value: u128,
}
impl UintNote {
pub fn new(value: u128, owner: AztecAddress) -> Self {
- let randomness = unsafe { random() };
- Self { value, owner, randomness }
+ Self { value, owner }
}
Add randomness to compute_note_hash implementation
The NoteHash trait now requires compute_note_hash to receive a randomness field. This impacts
pub trait NoteHash {
/// ...
- fn compute_note_hash(self, storage_slot: Field) -> Field;
+ fn compute_note_hash(self, storage_slot: Field, randomness: Field) -> Field;
Then in trait implementations:
impl NoteHash for UintNote {
- fn compute_note_hash(self, storage_slot: Field) -> Field {
+ fn compute_note_hash(self, storage_slot: Field, randomness: Field) -> Field {
/// ...
- let private_content =
- UintPartialNotePrivateContent { owner: self.owner, randomness: self.randomness };
- let partial_note = PartialUintNote {
- commitment: private_content.compute_partial_commitment(storage_slot),
- };
+ let private_content =
+ UintPartialNotePrivateContent { owner: self.owner };
+ let partial_note = PartialUintNote {
+ commitment: private_content.compute_partial_commitment(storage_slot, randomness),
+ };
It's worth noting that this change also affects how partial notes are structured and handled.
pub fn partial(
owner: AztecAddress,
storage_slot: Field,
randomness: Field,
context: &mut PrivateContext,
recipient: AztecAddress,
completer: AztecAddress,
) -> PartialUintNote {
- let commitment = UintPartialNotePrivateContent { owner, randomness }
- .compute_partial_commitment(storage_slot);
+ let commitment = UintPartialNotePrivateContent { owner }
+ .compute_partial_commitment(storage_slot, randomness);
let private_log_content =
- UintPartialNotePrivateLogContent { owner, randomness, public_log_tag: commitment };
+ UintPartialNotePrivateLogContent { owner, public_log_tag: commitment };
let encrypted_log = note::compute_partial_note_private_content_log(
private_log_content,
storage_slot,
+ randomness,
recipient,
);
/// ...
}
struct UintPartialNotePrivateContent {
owner: AztecAddress,
- randomness: Field,
}
impl UintPartialNotePrivateContent {
- fn compute_partial_commitment(self, storage_slot: Field) -> Field {
+ fn compute_partial_commitment(self, storage_slot: Field, randomness: Field) -> Field {
poseidon2_hash_with_separator(
- self.pack().concat([storage_slot]),
+ self.pack().concat([storage_slot, randomness]),
DOM_SEP__NOTE_HASH,
)
}
}
struct UintPartialNotePrivateLogContent {
public_log_tag: Field,
owner: AztecAddress,
- randomness: Field,
}
Note size
As a result of this change, the maximum packed length of the content of a note is 11 fields, down from 12. This is a direct consequence of moving the randomness field from the note content structure to the note's metadata.
HintedNote now includes randomness field
pub struct HintedNote<Note> {
pub note: Note,
pub contract_address: AztecAddress,
+ pub randomness: Field,
pub metadata: NoteMetadata,
}
[L1 Contracts] Block is now Checkpoint
A checkpoint is now the primary unit handled by the L1 contracts.
A checkpoint may contain one or more L2 blocks. The protocol circuits already support producing multiple blocks per checkpoint. Updating the L1 contracts to operate on checkpoints allow L2 blockchain to advance faster.
Below are the API and event renames reflecting this change:
- event L2BlockProposed
+ event CheckpointProposed
- event BlockInvalidated
+ event CheckpointInvalidated
- function getEpochForBlock(uint256 _blockNumber) external view returns (Epoch);
+ function getEpochForCheckpoint(uint256 _checkpointNumber) external view returns (Epoch);
- function getProvenBlockNumber() external view returns (uint256);
+ function getProvenCheckpointNumber() external view returns (uint256);
- function getPendingBlockNumber() external view returns (uint256);
+ function getPendingCheckpointNumber() external view returns (uint256);
- function getBlock(uint256 _blockNumber) external view returns (BlockLog memory);
+ function getCheckpoint(uint256 _checkpointNumber) external view returns (CheckpointLog memory);
- function getBlockReward() external view returns (uint256);
+ function getCheckpointReward() external view returns (uint256);
Additionally, any function or struct that previously referenced an L2 block number now uses a checkpoint number instead:
- function status(uint256 _blockNumber) external view returns (
+ function status(uint256 _checkpointNumber) external view returns (
- uint256 provenBlockNumber,
+ uint256 provenCheckpointNumber,
bytes32 provenArchive,
- uint256 pendingBlockNumber,
+ uint256 pendingCheckpointNumber,
bytes32 pendingArchive,
bytes32 archiveOfMyBlock,
Epoch provenEpochNumber
);
Note: current node softwares still produce exactly one L2 block per checkpoint, so for now checkpoint numbers and L2 block numbers remain equal. This may change once multi-block checkpoints are enabled.
[L1 Contracts] L2-to-L1 messages are now grouped by epoch.
L2-to-L1 messages are now aggregated and organized per epoch rather than per block. This change affects how you compute membership witnesses for consuming messages on L1. You now need to know the epoch number in which the message was emitted to retrieve and consume the message.
Note: This is only an API change. The protocol behavior remains the same - messages can still only be consumed once an epoch is proven as before.
What changed
Previously, you might have computed the membership witness without explicitly needing the epoch:
const witness = await computeL2ToL1MembershipWitness(
node,
l2TxReceipt.blockNumber,
l2ToL1Message,
);
Now, you should provide the epoch number:
const epoch = await rollup.getEpochNumberForCheckpoint(
CheckpointNumber.fromBlockNumber(l2TxReceipt.blockNumber),
);
const witness = await computeL2ToL1MembershipWitness(
node,
epoch,
l2ToL1Message,
);
[Aztec.js] Wallet interface changes
simulateTx is now batchable
The simulateTx method on the Wallet interface is now batchable, meaning it can be called as part of a batch operation using wallet.batch(). This allows you to batch simulations together with other wallet operations like registerContract, sendTx, and registerSender.
- // Could not batch simulations
- const simulationResult = await wallet.simulateTx(executionPayload, options);
+ // Can now batch simulations with other operations
+ const results = await wallet.batch([
+ { name: 'registerContract', args: [instance, artifact] },
+ { name: 'simulateTx', args: [executionPayload, options] },
+ { name: 'sendTx', args: [anotherPayload, sendOptions] },
+ ]);
ExecutionPayload moved to @aztec/stdlib/tx
The ExecutionPayload type has been moved from @aztec/aztec.js to @aztec/stdlib/tx. Update your imports accordingly.
- import { ExecutionPayload } from '@aztec/aztec.js';
+ import { ExecutionPayload } from '@aztec/stdlib/tx';
+ // Or import from the re-export in aztec.js/tx:
+ import { ExecutionPayload } from '@aztec/aztec.js/tx';
ExecutionPayload now includes feePayer property
The ExecutionPayload class now includes an optional feePayer property that specifies which address is paying for the fee in the execution payload (if any)
const payload = new ExecutionPayload(
calls,
authWitnesses,
capsules,
extraHashedArgs,
+ feePayer // optional AztecAddress
);
This was previously provided as part of the SendOptions (and others) in the wallet interface, which could cause problems if a payload was assembled with a payment method and the parameter was later omitted. This means SendOptions now loses embeddedPaymentMethodFeePayer
-wallet.simulateTx(executionPayload, { from: address, embeddedFeePaymentMethodFeePayer: feePayer });
+wallet.simulateTx(executionPayload, { from: address });
simulateUtility signature and return type changed
The simulateUtility method signature has changed to accept a FunctionCall object instead of separate functionName, args, and to parameters. Additionally, the return type has changed from AbiDecoded to Fr[].
- const result: AbiDecoded = await wallet.simulateUtility(functionName, args, to, authWitnesses);
+ const result: UtilitySimulationResult = await wallet.simulateUtility(functionCall, authWitnesses?);
+ // result.result is now Fr[] instead of AbiDecoded
The new signature takes:
functionCall: AFunctionCallobject containingname,args,to,selector,type,isStatic,hideMsgSender, andreturnTypesauthWitnesses(optional): An array ofAuthWitnessobjects
The first argument is exactly the same as what goes into ExecutionPayload.calls. As such, the data is already encoded. The return value is now UtilitySimulationResult with result: Fr[] instead of returning an AbiDecoded value directly. You'll need to decode the Fr[] array yourself if you need typed results.
Contract.at() is now synchronous and no longer calls registerContract
The Contract.at() method (and generated contract .at() methods) is now synchronous and no longer automatically registers the contract with the wallet. This reduces unnecessary artifact storage and RPC calls.
- const contract = await TokenContract.at(address, wallet);
+ const contract = TokenContract.at(address, wallet);
Important: You now need to explicitly call registerContract if you want the wallet to store the contract instance and artifact. This is only necessary when:
- An app first registers a contract
- An app tries to update a contract's artifact
If you need to register the contract, do so explicitly:
// Get the instance from deployment
const { contract, instance } = await TokenContract.deploy(wallet, ...args)
.send({ from: address })
.wait();
// wallet already has it registered, since the deploy method does it by default
// to avoid it, set skipContractRegistration: true in the send options.
// Register it with another wallet
await otherWallet.registerContract(instance, TokenContract.artifact);
// Now you can use the contract
const otherContract = TokenContract.at(instance.address, otherWallet);
Publicly deployed contract instances can be retrieved via node.getContract(address). Otherwise and if deployment parameters are known, an instance can be computed via the getContractInstanceFromInstantiationParams from @aztec/aztec.js/contracts
registerContract signature simplified
The registerContract method now takes a ContractInstanceWithAddress instead of a Contract object, and the artifact parameter is now optional. If the artifact is not provided, the wallet will attempt to look it up from its contract class storage.
- await wallet.registerContract(contract);
+ await wallet.registerContract(instance, artifact?);
The method now only accepts:
instance: AContractInstanceWithAddressobjectartifact(optional): AContractArtifactobjectsecretKey(optional): A secret key for privacy keys registration
Return value of getNotes no longer contains a recipient and it contains some other additional info
Return value of getNotes used to be defined as Promise<UniqueNote[]> and is now defined as Promise<NoteDao[]>.
NoteDao is mostly a super-set of UniqueNote but it doesn't contain a recipient.
Having the recipient in the return value has been redundant as the same outcome can be achieved by populating the scopes array in NoteFilter with the recipient value.
Changes to getPrivateEvents
The signature of getPrivateEvents has changed for two reasons:
- To align it with how other query methods that include filtering by block range work (for example,
AztecNode#getPublicLogs) - To enrich the returned private events with metadata.
getPrivateEvents<T>(
- contractAddress: AztecAddress,
- eventMetadata: EventMetadataDefinition,
- from: number,
- numBlocks: number,
- recipients: AztecAddress[],
- ): Promise<T[]>;
+ eventFilter: PrivateEventFilter,
+ ): Promise<PrivateEvent<T>[]>;
PrivateEvent<T> bundles together an ABI decoded event of type T, with metadata of type InTx:
export type InBlock = {
l2BlockNumber: BlockNumber;
l2BlockHash: L2BlockHash;
};
export type InTx = InBlock & {
txHash: TxHash;
};
export type PrivateEvent<T> = {
event: T;
metadata: InTx;
};
You will need to update any calls to Wallet#getPrivateEvents accordingly. See below for before/after comparison which conserves
semantics.
Pay special attention to the fact that the old method expects a numBlocks parameter that instructs it to
return numBlocks blocks after fromBlock, whereas the new version expects an (exclusive) toBlock block number.
Also note we're replacing recipient terminology with scope. While underlying data types are equivalent (they are Aztec addresses), they have different semantics. Messages have a recipient who will be able to receive and process them. As a result of processing messages for a given recipient address, PXE might discover events. Those events are then said to be in scope for that address.
- const events = await context.client.getPrivateEvents(contractAddress, eventMetadata, 42, 10, [recipient]);
- doSomethingWithAnEvent(events[0]);
+ const events = await context.client.getPrivateEvents(eventMetadata, {
+ contractAddress,
+ fromBlock: BlockNumber(42),
+ toBlock: BlockNumber(42 + 10),
+ scopes: [scope],
+ });
+ doSomethingWithAnEvent(events[0].event);
Please refer to the wallet interface js-docs for further details.
[CLI] Command refactor
The sandbox command has been renamed and remapped to "local network". We believe this conveys better what is actually being spun up when running it.
REMOVED/RENAMED:
aztec start --sandbox: nowaztec start --local-network
[Aztec.nr] - Contract API redesign
In this release we decided to largely redesign our contract API. Most of the changes here are not a breaking change
(only renaming of original #[internal] to #[only_self] and storage now being available on the newly introduced
self struct are a breaking change).
1. Renaming of original #[internal] as #[only_self]
We want for internal to mean the same as in Solidity where internal function can be called only from the same contract
and is also inlined (EVM JUMP opcode and not EVM CALL). The original implementation of our #[internal] macro also
results in the function being callable only from the same contract but it results in a different call (hence it doesn't
map to EVM JUMP). This is very confusing for people that know Solidity hence we are doing the rename. A true
#[internal] will be introduced in the future.
To migrate your contracts simply rename all the occurrences of #[internal] with #[only_self] and update the imports:
- use aztec::macros::functions::internal;
+ use aztec::macros::functions::only_self;
#[external("public")]
- #[internal]
+ #[only_self]
fn _deduct_public_balance(owner: AztecAddress, amount: u64) {
...
}
2. Introducing of new #[internal]
Same as in Solidity internal functions are functions that are callable from inside the contract. Unlike #[only_self] functions, internal functions are inlined (e.g. akin to EVM's JUMP and not EVM's CALL).
Internal function can be called using the following API which leverages the new self struct (see change 3 below for
details):
self.internal.my_internal_function(...)
Private internal functions can only be called from other private external or internal functions. Public internal functions can only be called from other public external or internal functions.
3. Introducing self in contracts and a new call interface
Aztec contracts now automatically inject a self parameter into every contract function, providing a unified interface
for accessing the contract's address, storage, calling of function and an execution context.
What is self?
self is an instance of ContractSelf<Context, Storage> that provides:
self.address- The contract's own addressself.storage- Access to your contract's storageself.context- The execution context (private, public, or utility)self.msg_sender()- Get the address of the callerself.emit(...)- Emit eventsself.call(...)- Call an external functionself.view(...)- Call an external function staticallyself.enqueue(...)- Enqueue a call to an external functionself.enqueue_view(...)- Enqueue a call to an external functionself.enqueue_incognito(...)- Enqueue a call to an external function but hides themsg_senderself.enqueue_view_incognito(...)- Enqueue a static call to an external function but hides themsg_senderself.set_as_teardown(...)- Enqueue a call to an external public function and sets the call as teardownself.set_as_teardown_incognito(...)- Enqueue a call to an external public function and sets the call as teardown and hides themsg_senderself.internal.my_internal_fn(...)- Call an internal function
self also provides you with convenience API to call and enqueue calls to external functions from within the same
contract (this is just a convenience API as self.call(MyContract::at(self.address).my_external_fn(...)) would also
work):
self.call_self.my_external_fn(...)- Call external function from within the same contractself.enqueue_self.my_public_external_fn(...)self.call_self_static.my_static_external_fn(...)self.enqueue_self_static.my_static_external_public_fn(...)
How it works
The #[external(...)] macro automatically injects self into your function. When you write:
#[external("private")]
fn transfer(amount: u128, recipient: AztecAddress) {
let sender = self.msg_sender().unwrap();
self.storage.balances.at(sender).sub(amount);
self.storage.balances.at(recipient).add(amount);
}
The macro transforms it to initialize self with the context and storage before your code executes.
Migration guide
Before: Access context and storage as separate parameters
#[external("private")]
fn old_transfer(amount: u128, recipient: AztecAddress) {
let storage = Storage::init(context);
let sender = context.msg_sender().unwrap();
storage.balances.at(sender).sub(amount);
}
After: Use self to access everything
#[external("private")]
fn new_transfer(amount: u128, recipient: AztecAddress) {
let sender = self.msg_sender().unwrap();
self.storage.balances.at(sender).sub(amount);
}
Key changes
- Storage and context access:
Storage and context are no longer injected into the function as standalone variables and instead you need to access them via self:
- let balance = storage.balances.at(owner).read();
+ let balance = self.storage.balances.at(owner).read();
- context.push_nullifier(nullifier);
+ self.context.push_nullifier(nullifier);
Note that context is expected to be use only when needing to access a low-level API (like directly emitting a nullifier).
-
Getting caller address: Use
self.msg_sender()instead ofcontext.msg_sender()- let caller = context.msg_sender().unwrap();
+ let caller = self.msg_sender().unwrap(); -
Getting contract address: Use
self.addressinstead ofcontext.this_address()- let this_contract = context.this_address();
+ let this_contract = self.address; -
Emitting events:
In private functions:
- emit_event_in_private(event, context, recipient, delivery_mode);
+ self.emit(event, recipient, delivery_mode);In public functions:
- emit_event_in_public(event, context);
+ self.emit(event); -
Calling functions:
In private functions:
- Token::at(stable_coin).mint_to_public(to, amount).call(&mut context);
+ self.call(Token::at(stable_coin).mint_to_public(to, amount));
Example: Full contract migration
Before:
#[external("private")]
fn withdraw(amount: u128, recipient: AztecAddress) {
let storage = Storage::init(context);
let sender = context.msg_sender().unwrap();
let token = storage.donation_token.get_note().get_address();
// ... withdrawal logic
emit_event_in_private(Withdraw { withdrawer, amount }, context, withdrawer, MessageDelivery.UNCONSTRAINED_ONCHAIN);
}
After:
#[external("private")]
fn withdraw(amount: u128, recipient: AztecAddress) {
let sender = self.msg_sender().unwrap();
let token = self.storage.donation_token.get_note().get_address();
// ... withdrawal logic
self.emit(Withdraw { withdrawer, amount }, withdrawer, MessageDelivery.UNCONSTRAINED_ONCHAIN);
}
No-longer allowing calling of non-view function statically via the old higher-level API
We used to allow calling of non-view function statically as follows:
MyContract::at(address).my_non_view_function(...).view(context);
MyContract::at(address).my_non_view_function(...).enqueue_view(context);
This is no-longer allowed and if you will want to call a function statically you will need to mark the function with
#[view].
Phase checks
Now private external functions check by default that no phase change from non revertible to revertible happens during the execution of the function or any of its nested calls. If you're developing a function
that handles phase change (you call context.end_setup() or call a function that you expect will change phase) you need to opt out of the phase check using the #[nophasecheck] macro. Also, now it's possible to know if you're in the revertible phase of the transaction at any point using self.context.in_revertible_phase().
[aztec command] Moving functionality of aztec-nargo to aztec command
aztec-nargo has been deprecated and all workflows should now migrate to the aztec command that fully replaces aztec-nargo:
-
For contract initialization:
aztec init(Behaves like
nargo init, but defaults to a contract project.) -
For testing:
aztec test(Starts the Aztec TXE and runs your tests.)
-
For compiling contracts:
aztec compile(Transpiles your contracts and generates verification keys.)
3.0.0-devnet.4
[aztec.js] Removal of barrel export
aztec.js is now divided into granular exports, which improves loading performance in node.js and also makes the job of web bundlers easier:
-import { AztecAddress, Fr, getContractInstanceFromInstantiationParams, type Wallet } from '@aztec/aztec.js';
+import { AztecAddress } from '@aztec/aztec.js/addresses';
+import { getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts';
+import { Fr } from '@aztec/aztec.js/fields';
+import type { Wallet } from '@aztec/aztec.js/wallet';
Additionally, some general utilities reexported from foundation have been removed:
-export { toBigIntBE } from '@aztec/foundation/bigint-buffer';
-export { sha256, Grumpkin, Schnorr } from '@aztec/foundation/crypto';
-export { makeFetch } from '@aztec/foundation/json-rpc/client';
-export { retry, retryUntil } from '@aztec/foundation/retry';
-export { to2Fields, toBigInt } from '@aztec/foundation/serialize';
-export { sleep } from '@aztec/foundation/sleep';
-export { elapsed } from '@aztec/foundation/timer';
-export { type FieldsOf } from '@aztec/foundation/types';
-export { fileURLToPath } from '@aztec/foundation/url';
getSenders renamed to getAddressBook in wallet interface
An app could request "contacts" from the wallet, which don't necessarily have to be senders in the wallet's PXE. This method has been renamed to reflect that fact:
-wallet.getSenders();
+wallet.getAddressBook();
Removal of proveTx from Wallet interface
Exposing this method on the interface opened the door for certain types of attacks, were an app could route proven transactions through malicious nodes (that stored them for later decryption, or collected user IPs for example). It also made transactions difficult to track for the wallet, since they could be sent without their knowledge at any time. This change also affects ContractFunctionInteraction and DeployMethod, which no longer expose a prove() method.
msg_sender is now an Option<AztecAddress> type.
Because Aztec has native account abstraction, the very first function call of a tx has no msg_sender. (Recall, the first function call of an Aztec transaction is always a private function call).
Previously (before this change) we'd been silently setting this first msg_sender to be AztecAddress::from_field(-1);, and enforcing this value in the protocol's kernel circuits. Now we're passing explicitness to smart contract developers by wrapping msg_sender in an Option type. We'll explain the syntax shortly.
We've also added a new protocol feature. Previously (before this change) whenever a public function call was enqueued by a private function (a so-called private->public call), the called public function (and hence the whole world) would be able to see msg_sender. For some use cases, visibility of msg_sender is important, to ensure the caller executed certain checks in private-land. For #[only_self] public functions, visibility of msg_sender is unavoidable (the caller of an #[only_self] function must be the same contract address by definition). But for some use cases, a visible msg_sender is an unnecessary privacy leakage.
We therefore have added a feature where msg_sender can be optionally set to Option<AztecAddress>::none() for enqueued public function calls (aka private->public calls). We've been colloquially referring to this as "setting msg_sender to null".
Aztec.nr diffs
Note: we'll be doing another pass at this aztec.nr syntax in the near future.
Given the above, the syntax for accessing msg_sender in Aztec.nr is slightly different:
For most public and private functions, to adjust to this change, you can make this change to your code:
- let sender: AztecAddress = context.msg_sender();
+ let sender: AztecAddress = context.msg_sender().unwrap();
Recall that Option::unwrap() will throw if the Option is "none".
Indeed, most smart contract functions will require access to a proper contract address (instead of a "null" value), in order to do bookkeeping (allocation of state variables against user addresses), and so in such cases throwing is sensible behaviour.
If you want to output a useful error message when unwrapping fails, you can use Option::expect:
- let sender: AztecAddress = context.msg_sender();
+ let sender: AztecAddress = context.msg_sender().expect(f"Sender must not be none!");
For a minority of functions, a "null" msg_sender will be acceptable:
- A private entrypoint function.
- A public function which doesn't seek to do bookkeeping against
msg_sender.
Some apps might even want to assert that the msg_sender is "null" to force their users into strong privacy practices:
let sender: Option<AztecAddress> = context.msg_sender();
assert(sender.is_none());
Enqueueing public function calls
Auto-generated contract interfaces
When you use the #[aztec] macro, it will generate a noir contract interface for your contract, behind the scenes.
This provides pretty syntax when you come to call functions of that contract. E.g.:
Token::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context);
In keeping with this new feature of being able to enqueue public function calls with a hidden msg_sender, there are some new methods that can be chained instead of .enqueue(...):
enqueue_incognito-- akin toenqueue, butmsg_senderis set "null".enqueue_view_incognito-- akin toenqueue_view, butmsg_senderis "null".set_as_teardown_incognito-- akin toset_as_teardown, butmsg_senderis "null".
The name "incognito" has been chosen to imply "msg_sender will not be visible to observers".
These new functions enable the calling contract to specify that it wants its address to not be visible to the called public function. This is worth re-iterating: it is the caller's choice. A smart contract developer who uses these functions must be sure that the target public function will accept a "null" msg_sender. It would not be good (for example) if the called public function did context.msg_sender().unwrap(), because then a public function that is called via enqueue_incognito would always fail! Hopefully smart contract developers will write sufficient tests to catch such problems during development!
Making lower-level public function calls from the private context
This is discouraged vs using the auto-generated contract interfaces described directly above.
If you do use any of these low-level methods of the PrivateContext in your contract:
call_public_functionstatic_call_public_functioncall_public_function_no_argsstatic_call_public_function_no_argscall_public_function_with_calldata_hashset_public_teardown_functionset_public_teardown_function_with_calldata_hash
... there is a new hide_msg_sender: bool parameter that you will need to specify.
Aztec.js diffs
Note: we'll be doing another pass at this aztec.js syntax in the near future.
When lining up a new tx, the FunctionCall struct has been extended to include a hide_msg_sender: bool field.
is_public & hide_msg_sender-- will make a public call withmsg_senderset to "null".is_public & !hide_msg_sender-- will make a public call with a visiblemsg_sender, as was the case before this new feature.!is_public & hide_msg_sender-- Incompatible flags.!is_public & !hide_msg_sender-- will make a private call with a visiblemsg_sender(noting that since it's a private function call, themsg_senderwill only be visible to the called private function, but not to the rest of the world).
[cli-wallet]
The deploy-account command now requires the address (or alias) of the account to deploy as an argument, not a parameter
+aztec-wallet deploy-account main
-aztec-wallet deploy-account -f main
This release includes a major architectural change to the system. The PXE JSON RPC Server has been removed, and PXE is now available only as a library to be used by wallets.
[Aztec node]
Network config. The node now pulls default configuration from the public repository AztecProtocol/networks after it applies the configuration it takes from the running environment and the configuration values baked into the source code. See associated Design document
[Aztec.js]
Removing Aztec cheatcodes
The Aztec cheatcodes class has been removed. Its functionality can be replaced by using the getNotes(...) function directly available on our TestWallet, along with the relevant functions available on the Aztec Node interface (note that the cheatcodes were generally just a thin wrapper around the Aztec Node interface).
CLI Wallet commands dropped from aztec command
The following commands used to be exposed by both the aztec and the aztec-wallet commands:
- import-test-accounts
- create-account
- deploy-account
- deploy
- send
- simulate
- profile
- bridge-fee-juice
- create-authwit
- authorize-action
- get-tx
- cancel-tx
- register-sender
- register-contract
These were dropped from aztec and now are exposed only by the cli-wallet command exposed by the @aztec/cli-wallet package.
PXE commands dropped from aztec command
The following commands were dropped from the aztec command:
add-contract: use can be replaced withregister-contracton ourcli-walletget-contract-data: debug-only and not considered important enough to need a replacementget-accounts: debug-only and can be replaced by loading aliases fromcli-walletget-account: debug-only and can be replaced by loading aliases fromcli-walletget-pxe-info: debug-only and not considered important enough to need a replacement
[Aztec.nr]
Replacing #[private], #[public], #[utility] with #[external(...)] macro
The original naming was not great in that it did not sufficiently communicate what the given macro did.
We decided to rename #[private] as #[external("private")], #[public] as #[external("public")], and #[utility] as #[external("utility")] to better communicate that these functions are externally callable and to specify their execution context. In this sense, external now means the exact same thing as in Solidity, i.e. a function that can be called from other contracts, and that can only be invoked via a contract call (i.e. the CALL opcode in the EVM, and a kernel call/AVM CALL opcode in Aztec).
You have to do the following changes in your contracts:
Update import:
- use aztec::macros::functions::private;
- use aztec::macros::functions::public;
- use aztec::macros::functions::utility;
+ use aztec::macros::functions::external;
Update attributes of your functions:
- #[private]
+ #[external("private")]
fn my_private_func() {
- #[public]
+ #[external("public")]
fn my_public_func() {
- #[utility]
+ #[external("utility")]
fn my_utility_func() {
Dropping remote mutable references to public context
PrivateContext generally needs to be passed as a mutable reference to functions because it does actually hold state
we're mutating. This is not the case for PublicContext, or UtilityContext - these are just marker objects that
indicate the current execution mode and make available the correct subset of the API. For this reason we have dropped
the mutable reference from the API.
If you've passed the context as an argument to custom functions you will need to do the following migration (example from our token contract):
#[contract_library_method]
fn _finalize_transfer_to_private(
from_and_completer: AztecAddress,
amount: u128,
partial_note: PartialUintNote,
- context: &mut PublicContext,
- storage: Storage<&mut PublicContext>,
+ context: PublicContext,
+ storage: Storage<PublicContext>,
) {
...
}
Authwit Test Helper now takes env
The add_private_authwit_from_call_interface test helper available in test::helpers::authwit now takes a TestEnvironment parameter, mirroring add_public_authwit_from_call_interface. This adds some unfortunate verbosity, but there are bigger plans to improve authwit usage in Noir tests in the near future.
add_private_authwit_from_call_interface(
+ env,
on_behalf_of,
caller,
call_interface,
);
Historical block renamed as anchor block
A historical block term has been used as a term that denotes the block against which a private part of a tx has been executed. This name is ambiguous and for this reason we've introduced "anchor block". This naming change resulted in quite a few changes and if you've access private context's or utility context's block header you will need to update your code:
- let header = context.get_block_header();
+ let header = context.get_anchor_block_header();
Removed ValueNote utils
The value_note::utils module has been removed because it was incorrect to have those in the value note package.
For the increment function you can easily just insert the note:
- use value_note::utils;
- utils::increment(storage.notes.at(owner), value, owner, sender);
+ let note = ValueNote::new(value, owner);
+ storage.notes.at(owner).insert(note).emit(&mut context, owner, MessageDelivery.CONSTRAINED_ONCHAIN);
PrivateMutable: replace / initialize_or_replace behaviour change
Motivation:
Updating a note used to require reading it first (via get_note, which nullifies and recreates it) and then calling replace — effectively proving a note twice. Now, replace accepts a callback that transforms the current note directly, and initialize_or_replace simply uses this updated replace internally. This reduces circuit cost while maintaining exactly one current note.
Key points:
replace(self, new_note)(old) →replace(self, f)(new), whereftakes the current note and returns a transformed note.initialize_or_replace(self, note)(old) →initialize_or_replace(self, f)(new), whereftakes anOptionwith the current note, ornoneif uninitialized.- Previous note is automatically nullified before the new note is inserted.
NoteEmission<Note>still requires.emit()or.discard().
Example Migration:
- let current_note = storage.my_var.get_note();
- let new_note = f(current_note);
- storage.my_var.replace(new_note);
+ storage.my_var.replace(|current_note| f(current_note));
- storage.my_var.initialize_or_replace(new_note);
+ storage.my_var.initialize_or_replace(|_| new_note);
This makes it easy and efficient to handle both initialization and current value mutation via initialize_or_replace, e.g. if implementing a note that simply counts how many times it has been read:
+ storage.my_var.initialize_or_replace(|opt_current: Option<Note>| opt_current.unwrap_or(0 /* initial value */) + 1);
- The callback can be a closure (inline) or a named function.
- Any previous assumptions that replace simply inserts a new_note directly must be updated.
Unified oracles into single get_utility_context oracle
The following oracles:
- get_contract_address,
- get_block_number,
- get_timestamp,
- get_chain_id,
- get_version
were replaced with a single get_utility_context oracle whose return value contains all the values returned from the removed oracles.
If you have used one of these removed oracles before, update the import, e.g.:
- aztec::oracle::execution::get_chain_id;
+ aztec::oracle::execution::get_utility_context
and get the value out of the returned utility context:
- let chain_id = get_chain_id();
+ let chain_id = get_utility_context().chain_id();
Note emission API changes
The note emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are:
encode_and_encrypt_notehas been removed in favor of callingemitdirectly withMessageDelivery.CONSTRAINED_ONCHAINencode_and_encrypt_note_unconstrainedhas been removed in favor of callingemitdirectly withMessageDelivery.UNCONSTRAINED_ONCHAINencode_and_encrypt_note_and_emit_as_offchain_messagehas been removed in favor of usingemitwithMessageDelivery.UNCONSTRAINED_OFFCHAIN- Note emission now takes a
delivery_modeparameter with the following values:CONSTRAINED_ONCHAIN: For onchain delivery with cryptographic guarantees that recipients can discover and decrypt messages. Uses constrained encryption but is slower to prove. Best for critical messages that contracts need to verify.UNCONSTRAINED_ONCHAIN: For onchain delivery without encryption constraints. Faster proving but trusts the sender. Good when the sender is incentivized to perform encryption correctly (e.g. they are buying something and will only get it if the recipient sees the note). No guarantees that recipients will be able to find or decrypt messages.UNCONSTRAINED_OFFCHAIN: For offchain delivery (e.g. cloud storage) without constraints. Lowest cost since no onchain storage needed. Requires custom infrastructure for delivery. No guarantees that messages will be delivered or that recipients will ever find them.
- The
contextobject no longer needs to be passed to these functions
Example migration:
First you need to update imports in your contract:
- aztec::messages::logs::note::encode_and_encrypt_note;
- aztec::messages::logs::note::encode_and_encrypt_note_unconstrained;
- aztec::messages::logs::note::encode_and_encrypt_note_and_emit_as_offchain_message;
+ aztec::messages::message_delivery::MessageDelivery;
Then update the emissions:
- storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note(&mut context, from));
+ storage.balances.at(from).sub(from, amount).emit(&mut context, from, MessageDelivery.CONSTRAINED_ONCHAIN);
- storage.balances.at(from).add(from, change).emit(encode_and_encrypt_note_unconstrained(&mut context, from));
+ storage.balances.at(from).add(from, change).emit(&mut context, from, MessageDelivery.UNCONSTRAINED_ONCHAIN);
- storage.balances.at(owner).insert(note).emit(encode_and_encrypt_note_and_emit_as_offchain_message(&mut context, context.msg_sender());
+ storage.balances.at(owner).insert(note).emit(&mut context, context.msg_sender(), MessageDelivery.UNCONSTRAINED_OFFCHAIN);
2.0.2
[Public functions]
The L2 gas cost of the different AVM opcodes have been updated to reflect more realistic proving costs. Developers should review the L2 gas costs of executing public functions and reevaluate any hardcoded L2 gas limits.
[Aztec Tools]
Contract compilation now requires two steps
The aztec-nargo command is now a direct pass-through to vanilla nargo, without any special compilation flags or postprocessing. Contract compilation for Aztec now requires two explicit steps:
- Compile your contracts with
aztec-nargo compile - Run postprocessing with the new
aztec-postprocess-contractcommand
The postprocessing step includes:
- Transpiling functions for the Aztec VM
- Generating verification keys for private functions
- Caching verification keys for faster subsequent compilations
Update your build scripts accordingly:
- aztec-nargo compile
+ aztec-nargo compile
+ aztec-postprocess-contract
If you're using the aztec-up installer, the aztec-postprocess-contract command will be automatically installed alongside aztec-nargo.
[Aztec.js] Mandatory from
As we prepare for a bigger Wallet interface refactor and the upcoming WalletSDK, a new parameter has been added to contract interactions, which now should indicate explicitly the address of the entrypoint (usually the account contract) that will be used to authenticate the request. This will be checked in runtime against the current this.wallet.getAddress() value, to ensure consistent behavior while the rest of the API is reworked.
- await contract.methods.my_func(arg).send().wait();
+ await contract.methods.my_func(arg).send({ from: account1Address }).wait();
[Aztec.nr]
emit_event_in_public_log function renamed as emit_event_in_public
This change was done to make the naming consistent with the private counterpart (emit_event_in_private).
Private event emission API changes
The private event emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are:
emit_event_in_private_loghas been renamed toemit_event_in_privateand now takes adelivery_modeparameter instead ofconstraintsemit_event_as_offchain_messagehas been removed in favor of usingemit_event_in_privatewithMessageDelivery.UNCONSTRAINED_OFFCHAINPrivateLogContentenum has been replaced withMessageDeliveryenum with the following values:CONSTRAINED_ONCHAIN: For onchain delivery with cryptographic guarantees that recipients can discover and decrypt messages. Uses constrained encryption but is slower to prove. Best for critical messages that contracts need to verify.UNCONSTRAINED_ONCHAIN: For onchain delivery without encryption constraints. Faster proving but trusts the sender. Good when the sender is incentivized to perform encryption correctly (e.g. they are buying something and will only get it if the recipient sees the note). No guarantees that recipients will be able to find or decrypt messages.UNCONSTRAINED_OFFCHAIN: For offchain delivery (e.g. cloud storage) without constraints. Lowest cost since no onchain storage needed. Requires custom infrastructure for delivery. No guarantees that messages will be delivered or that recipients will ever find them.
Contract functions can no longer be pub or pub(crate)
With the latest changes to TestEnvironment, making contract functions have public visibility is no longer required given the new call_public and simulate_utility functions. To avoid accidental direct invocation, and to reduce confusion with the autogenerated interfaces, we're forbidding them being public.
- pub(crate) fn balance_of_private(account: AztecAddress) -> 128 {
+ fn balance_of_private(account: AztecAddress) -> 128 {
Notes require you to manually implement or derive Packable
We have decided to drop auto-derivation of Packable from the #[note] macro because we want to make the macros less magical.
With this change you will be forced to either apply #[derive(Packable) on your notes:
+use aztec::protocol::traits::Packable;
+#[derive(Packable)]
#[note]
pub struct UintNote {
owner: AztecAddress,
randomness: Field,
value: u128,
}
or to implement it manually yourself:
impl Packable for UintNote {
let N: u32 = 3;
fn pack(self) -> [Field; Self::N] {
[self.owner.to_field(), randomness, value as Field]
}
fn unpack(fields: [Field; Self::N]) -> Self {
let owner = AztecAddress::from_field(fields[0]);
let randomness = fields[1];
let value = fields[2] as u128;
UintNote { owner, randomness, value }
}
}
Tagging sender now managed via oracle functions
Now, instead of manually needing to pass a tagging sender as an argument to log emission functions (e.g. encode_and_encrypt_note, encode_and_encrypt_note_unconstrained, emit_event_in_private_log, ...) we automatically load the sender via the get_sender_for_tags() oracle.
This value is expected to be populated by account contracts that should call set_sender_for_tags() in their entry point functions.
The changes you need to do in your contracts are quite straightforward.
You simply need to drop the sender arg from the callsites of the log emission functions.
E.g. note emission:
storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note(
&mut context,
from,
- tagging_sender,
));
E.g. private event emission:
emit_event_in_private_log(
Transfer { from, to, amount },
&mut context,
- tagging_sender,
to,
PrivateLogContent.NO_CONSTRAINTS,
);
This change affected arguments prepare_private_balance_increase and mint_to_private functions on the Token contract.
Drop the from argument when calling these.
Example in TypeScript test:
- await token.methods.mint_to_private(fundedWallet.getAddress(), alice, mintAmount).send().wait();
+ await token.methods.mint_to_private(alice, mintAmount).send().wait();
Example when
let token_out_partial_note = Token::at(token_out).prepare_private_balance_increase(
sender,
- tagging_sender
).call(&mut context);
SharedMutable -> DelayedPublicMutable
The SharedMutable state variable has been renamed to DelayedPublicMutable. It is a public mutable with a delay before state changes take effect. It can be read in private during the delay period. The name "shared" confuses developers who actually wish to work with so-called "shared private state". Also, we're working on a DelayedPrivateMutable which will have similar properties, except writes will be scheduled from private instead. With this new state variable in mind, the new name works nicely.
[TXE] - Testing Aztec Contracts using Noir
Full TestEnvironment API overhaul
As part of a broader effort to make Noir tests that leverage TXE easier to use and reason about, large parts of it were changed or adapted, resulting in the API now being quite different. No functionality was lost, so it should be possible to migrate any older Noir test to use the new API.
Network State Manipulation
committed_timestampremoved: this function did not work correctlyprivate_at_timestamp: this function was not really meaningful: private contexts are built from block numbers, not timestampspending_block_numberwas renamed tonext_block_number.pending_timestampwas removed since it was confusing and not usefulcommitted_block_numberwas renamed tolast_block_numberadvance_timestamp_toandadvance_timestamp_bywere renamed toset_next_block_timestampandadvance_next_block_timestamp_byrespectivelyadvance_block_towas renamed tomine_block_at, which takes a timestamp instead of a target block numberadvance_block_bywas renamed tomine_block, which now mines a single block
Account Management
create_accountwas renamed tocreate_light_accountcreate_account_contractwas renamed tocreate_contract_account
Contract Deployment
deploy_selfremoved: merged intodeploydeploynow accepts both local and external contracts
Contract Interactions
The old way of calling contract functions is gone. Contract functions are now invoked via the call_private, view_private, call_public, view_public and simulate_utility TestEnvironment methods. These take a CallInterface, like their old counterparts, but now also take an explicit from parameter (for the call variants - this is left out of the view and simulate methods for simplicity).
Raw Context Access
The private and public methods are gone. Private, public and utility contexts can now be crated with the private_context, public_context and utility_context functions, all of which takes a callback function that is called with the corresponding context. This functions are expected to be defined in-line as lambdas, and contain the user-defined test logic. This helps delineate where contexts begin and end. Contexts automatically mine blocks on closing, when appropriate.
Error-expecting Functions
assert_public_call_revert and variants have been removed. Use #[test(should_fail_with = "message")] instead.
Example Migration
The following are two tests using the older version of TestEnvironment:
#[test]
unconstrained fn initial_empty_value() {
let mut env = TestEnvironment::new();
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let admin = env.create_account(1);
let initializer_call_interface = Auth::interface().constructor(admin);
let auth_contract =
env.deploy_self("Auth").with_public_void_initializer(admin, initializer_call_interface);
let auth_contract_address = auth_contract.to_address();
env.impersonate(admin);
let authorized = Auth::at(auth_contract_address).get_authorized().view(&mut env.public());
assert_eq(authorized, AztecAddress::from_field(0));
}
#[test]
unconstrained fn non_admin_cannot_set_authorized() {
let mut env = TestEnvironment::new();
// Setup without account contracts. We are not using authwits here, so dummy accounts are enough
let admin = env.create_account(1);
let other = env.create_account(2);
let initializer_call_interface = Auth::interface().constructor(admin);
let auth_contract =
env.deploy_self("Auth").with_public_void_initializer(admin, initializer_call_interface);
let auth_contract_address = auth_contract.to_address();
env.impersonate(other);
env.assert_public_call_fails(Auth::at(auth_contract_address).set_authorized(to_authorize));
}
These now look like this:
#[test]
unconstrained fn authorized_initially_unset() {
let mut env = TestEnvironment::new();
let admin = env.create_light_account(); // Manual secret management gone
let auth_contract_address =
env.deploy("Auth").with_public_initializer(admin, Auth::interface().constructor(admin)); // deploy_self replaced
let auth = Auth::at(auth_contract_address);
assert_eq(env.view_public(auth.get_authorized()), AztecAddress::zero()); // .view_public() instead of .public()
}
#[test(should_fail_with = "caller is not admin")]
unconstrained fn non_admin_cannot_set_unauthorized() {
let mut env = TestEnvironment::new();
let admin = env.create_light_account();
let other = env.create_light_account();
let auth_contract_address =
env.deploy("Auth").with_public_initializer(admin, Auth::interface().constructor(admin)); // deploy_self replaced
let auth = Auth::at(auth_contract_address);
env.call_public(other, auth.set_authorized(other)); // .call_public(), should_fail_with
}
[Aztec.js]
Cheatcodes
Cheatcodes where moved out of the @aztec/aztec.js package to @aztec/ethereum and @aztec/aztec packages.
While all of the cheatcodes can be imported from the @aztec/aztec package EthCheatCodes and RollupCheatCodes reside in @aztec/ethereum package and if you need only those importing only that package should result in a lighter build.
Note exports dropped from artifact
Notes are no longer exported in the contract artifact. Exporting notes was technical debt from when we needed to interpret notes in TypeScript.
The following code will no longer work since notes is no longer available on the artifact:
const valueNoteTypeId = StatefulTestContractArtifact.notes['ValueNote'].id;
[core protocol, Aztec.nr, Aztec.js] Max block number property changed to be seconds based
max_block_number -> include_by_timestamp
The transaction expiration mechanism has been updated to use seconds rather than number of blocks.
As part of this change, the transaction property max_block_number has been renamed to include_by_timestamp.
This change significantly impacts the SharedMutable state variable in Aztec.nr, which now operates on a seconds instead of number of blocks.
If your contract uses SharedMutable, you'll need to:
- Update the
INITIAL_DELAYnumeric generic to use seconds instead of blocks - Modify any related logic to account for timestamp-based timing
- Note that timestamps use
u64values while block numbers useu32
Removed prelude, so your dep::aztec::prelude::... imports will need to be amended.
Instead of importing common types from dep::aztec::prelude..., you'll now need to import them from their lower-level locations.
The Noir Language Server vscode extension is now capable of autocompleting imports: just type some of the import and press 'tab' when it pops up with the correct item, and the import will be inserted at the top of the file.
As a quick reference, here are the paths to the types that were previously in the prelude.
So, for example, if you were previously using dep::aztec::prelude::AztecAddress, you'll need to replace it with dep::aztec::protocol::address::AztecAddress.
Apologies for any pain this brings. The reasoning is that these types were somewhat arbitrary, and it was unclear which types were worthy enough to be included here.
use dep::aztec::{
context::{PrivateCallInterface, PrivateContext, PublicContext, UtilityContext, ReturnsHash},
note::{
note_getter_options::NoteGetterOptions,
note_interface::{NoteHash, NoteType},
note_viewer_options::NoteViewerOptions,
hinted_note::HintedNote,
},
state_vars::{
map::Map, private_immutable::PrivateImmutable, private_mutable::PrivateMutable,
private_set::PrivateSet, public_immutable::PublicImmutable, public_mutable::PublicMutable,
shared_mutable::SharedMutable,
},
};
use dep::aztec::protocol::{
abis::function_selector::FunctionSelector,
address::{AztecAddress, EthAddress},
point::Point,
traits::{Deserialize, Serialize},
};
include_by_timestamp is now mandatory
Each transaction must now include a valid include_by_timestamp that satisfies the following conditions:
- It must be greater than the historical block’s timestamp.
- The duration between the
include_by_timestampand the historical block’s timestamp must not exceed the maximum allowed (currently 24 hours). - It must be greater than or equal to the timestamp of the block in which the transaction is included.
The protocol circuits compute the include_by_timestamp for contract updates during each private function iteration. If a contract does not explicitly specify a value, the default will be the maximum allowed duration. This ensures that include_by_timestamp is never left unset.
No client-side changes are required. However, please note that transactions now have a maximum lifespan of 24 hours and will be removed from the transaction pool once expired.
0.88.0
[Aztec.nr] Deprecation of the authwit library
It is now included in aztec-nr, so imports must be updated:
-dep::authwit::...
+dep::aztec::authwit...
and stale dependencies removed from Nargo.toml
-authwit = { path = "../../../../aztec-nr/authwit" }
0.87.0
[Aztec.js/TS libraries]
We've bumped our minimum supported node version to v20, as v18 is now EOL. As a consequence, the deprecated type assertion syntax has been replaced with modern import attributes whenever contract artifact JSONs are loaded:
-import ArtifactJson from '../artifacts/contract-Contract.json' assert { type: 'json' };
+import ArtifactJson from '../artifacts/contract-Contract.json' with { type: 'json' };
[Aztec.js/PXE] simulateUtility return type
pxe.simulateUtility() now returns a complex object (much like .simulateTx()) so extra information can be provided such as simulation timings.
This information can be accessed setting the includeMetadata flag in SimulateMethodOptions to true, but not providing it (which is the default) will NOT change the behavior of the current code.
-const result = await pxe.simulateUtility(...);
+const { meta, result } = await pxe.simulateUtility(...);
const result = await Contract.methods.myFunction(...).simulate();
const { result, meta} = await Contract.methods.myFunction(...).simulate({ includeMetadata: true });
[Aztec.js] Removed mandatory simulation before proving in contract interfaces
Previously, our autogenerated contract classes would perform a simulation when calling .prove or .send on them. This could potentially catch errors earlier, but took away control from the app/wallets on how to handle network interactions. Now this process has to be triggered manually, which means just proving an interaction (or proving and sending it to the network in one go via .send) is much faster.
WARNING: This means users can incurr in network fees if a transaction that would otherwise be invalid is sent without sanity checks. To ensure this, it is recommended to do:
+await Contract.method.simulate();
await Contract.method.send().wait();
0.86.0
[PXE] Removed PXE_L2_STARTING_BLOCK environment variable
PXE now fast-syncs by skipping finalized blocks and never downloads all blocks, so there is no longer a need to specify a starting block.
[Aztec.nr] Logs and messages renaming
The following renamings have taken place:
encrypted_logstomessages: this module now handles much more than just encrypted logs (including unconstrained message delivery, message encoding, etc.)log_assembly_strategiestologsdiscoverymoved tomessages: given that what is discovered are messagesdefault_aes128removed
Most contracts barely used these modules directly. The frequently used encode_and_encrypt function imports remain unchanged:
use dep::aztec::messages::logs::note::encode_and_encrypt_note;
[noir-contracts] Reference Noir contracts directory structure change
noir-projects/noir-contracts/contracts directory became too cluttered so we grouped contracts into account, app, docs, fees, libs, protocol and test dirs.
If you import contract from the directory make sure to update the paths accordingly.
E.g. for a token contract:
#[dependencies]
-token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.83.0", directory = "noir-projects/noir-contracts/contracts/src/token_contract" }
+token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v0.83.0", directory = "noir-projects/noir-contracts/contracts/app/src/token_contract" }
[Aztec.nr] #[utility] contract functions
Aztec contracts have three kinds of functions: #[private], #[public] and what was sometimes called 'top-level unconstrained': an unmarked unconstrained function in the contract module. These are now called [#utility] functions, and must be explicitly marked as such:
+ #[utility]
unconstrained fn balance_of_private(owner: AztecAddress) -> u128 {
storage.balances.at(owner).balance_of()
}
Utility functions are standalone unconstrained functions that cannot be called from private or public functions: they are meant to be called by applications to perform auxiliary tasks: query contract state (e.g. a token balance), process messages received offchain, etc.
All functions in a contract block must now be marked as one of either #[private], #[public], #[utility], #[contract_library_method], or #[test].
Additionally, the UnconstrainedContext type has been renamed to UtilityContext. This led us to rename the unkonstrained method on TestEnvironment to utility, so any tests using it also need updating:
- SharedMutable::new(env.unkonstrained(), storage_slot)
+ SharedMutable::new(env.utility(), storage_slot)
[AuthRegistry] function name change
As part of the broader transition from "top-level unconstrained" to "utility" name (detailed in the note above), the unconstrained_is_consumable function in AuthRegistry has been renamed to utility_is_consumable. The function's signature and behavior remain unchanged - only the name has been updated to align with the new convention. If you're currently using this function, a simple rename in your code will suffice.
0.83.0
[aztec.js] AztecNode.getPrivateEvents API change
The getPrivateEvents method signature has changed to require an address of a contract that emitted the event and use recipient addresses instead of viewing public keys:
- const events = await wallet.getPrivateEvents<Transfer>(TokenContract.events.Transfer, 1, 1, [recipient.getCompleteAddress().publicKeys.masterIncomingViewingPublicKey()]);
+ const events = await wallet.getPrivateEvents<Transfer>(token.address, TokenContract.events.Transfer, 1, 1, [recipient.getAddress()]);
[portal contracts] Versions and Non-following message boxes
The version number is no longer hard-coded to be 1 across all deployments (it not depends on where it is deployed to and with what genesis and logic).
This means that if your portal were hard-coding 1 it will now fail when inserting into the inbox or consuming from the outbox because of a version mismatch.
Instead you can get the real version (which don't change for a deployment) by reading the VERSION on inbox and outbox, or using getVersion() on the rollup.
New Deployments of the protocol do not preserve former state/across each other. This means that after a new deployment, any "portal" following the registry would try to send messages into this empty rollup to non-existent contracts. To solve, the portal should be linked to a specific deployment, e.g., a specific inbox. This can be done by storing the inbox/outbox/version at the time of deployment or initialize and not update them.
Both of these issues were in the token portal and the uniswap portal, so if you used them as a template it is very likely that you will also have it.
0.82.0
[aztec.js] AztecNode.findLeavesIndexes returns indexes with block metadata
It's common that we need block metadata of a block in which leaves were inserted when querying indexes of these tree leaves. For this reason we now return that information along with the indexes. This allows us to reduce the number of individual AztecNode queries.
Along with this change, findNullifiersIndexesWithBlock and findBlockNumbersForIndexes functions were removed as all their uses can now be replaced with the newly modified findLeavesIndexes function.
[aztec.js] AztecNode.getPublicDataTreeWitness renamed as AztecNode.getPublicDataWitness
This change was done to have consistent naming across codebase.
[aztec.js] Wallet interface and Authwit management
The Wallet interface in aztec.js is undergoing transformations, trying to be friendlier to wallet builders and reducing the surface of its API. This means Wallet no longer extends PXE, and instead just implements a subset of the methods of the former. This is NOT going to be its final form, but paves the way towards better interfaces and starts to clarify what the responsibilities of the wallet are:
/**
* The wallet interface.
*/
export type Wallet = AccountInterface &
Pick<
PXE,
// Simulation
| "simulateTx"
| "simulateUnconstrained"
| "profileTx"
// Sending
| "sendTx"
// Contract management (will probably be collapsed in the future to avoid instance and class versions)
| "getContractClassMetadata"
| "getContractMetadata"
| "registerContract"
| "registerContractClass"
// Likely to be removed
| "proveTx"
// Will probably be collapsed
| "getNodeInfo"
| "getPXEInfo"
// Fee info
| "getCurrentMinFees"
// Still undecided, kept for the time being
| "updateContract"
// Sender management
| "registerSender"
| "getSenders"
| "removeSender"
// Tx status
| "getTxReceipt"
// Events. Kept since events are going to be reworked and changes will come when that's done
| "getPrivateEvents"
| "getPublicEvents"
> & {
createAuthWit(intent: IntentInnerHash | IntentAction): Promise<AuthWitness>;
};
As a side effect, a few debug only features have been removed
// Obtain tx effects
const { txHash, debugInfo } = await contract.methods
.set_constant(value)
.send()
-- .wait({ interval: 0.1, debug: true });
++ .wait({ interval: 0.1 })
-- // check that 1 note hash was created
-- expect(debugInfo!.noteHashes.length).toBe(1);
++ const txEffect = await aztecNode.getTxEffect(txHash);
++ const noteHashes = txEffect?.data.noteHashes;
++ // check that 1 note hash was created
++ expect(noteHashes?.length).toBe(1);
// Wait for a tx to be proven
-- tx.wait({ timeout: 300, interval: 10, proven: true, provenTimeout: 3000 })));
++ const receipt = await tx.wait({ timeout: 300, interval: 10 });
++ await waitForProven(aztecNode, receipt, { provenTimeout: 3000 });
Authwit management has changed, and PXE no longer stores them. This is unnecessary because now they can be externally provided to simulations and transactions, making sure no stale authorizations are kept inside PXE's db.
const witness = await wallet.createAuthWit({ caller, action });
--await callerWallet.addAuthWitness(witness);
--await action.send().wait();
++await action.send({ authWitnesses: [witness] }).wait();
Another side effect of this is that the interface of the lookupValidity method has changed, and now the authwitness has to be provided:
const witness = await wallet.createAuthWit({ caller, action });
--await callerWallet.addAuthWitness(witness);
--await wallet.lookupValidity(wallet.getAddress(), { caller, action });
++await wallet.lookupValidity(wallet.getAddress(), { caller, action }, witness);
0.80.0
[PXE] Concurrent contract function simulation disabled
PXE is no longer be able to execute contract functions concurrently (e.g. by collecting calls to simulateTx and then using await Promise.all). They will instead be put in a job queue and executed sequentially in order of arrival.
0.79.0
[aztec.js] Changes to BatchCall and BaseContractInteraction
The constructor arguments of BatchCall have been updated to improve usability. Previously, it accepted an array of FunctionCall, requiring users to manually set additional data such as authwit and capsules. Now, BatchCall takes an array of BaseContractInteraction, which encapsulates all necessary information.
class BatchCall extends BaseContractInteraction {
- constructor(wallet: Wallet, protected calls: FunctionCall[]) {
+ constructor(wallet: Wallet, protected calls: BaseContractInteraction[]) {
...
}
The request method of BaseContractInteraction now returns ExecutionPayload. This object includes all the necessary data to execute one or more functions. BatchCall invokes this method on all interactions to aggregate the required information. It is also used internally in simulations for fee estimation.
Declaring a BatchCall:
new BatchCall(wallet, [
- await token.methods.transfer(alice, amount).request(),
- await token.methods.transfer_to_private(bob, amount).request(),
+ token.methods.transfer(alice, amount),
+ token.methods.transfer_to_private(bob, amount),
])
0.77.0
[aztec-nr] TestEnvironment::block_number() refactored
The block_number function from TestEnvironment has been expanded upon with two extra functions, the first being pending_block_number, and the second being committed_block_number. pending_block_number now returns what block_number does. In other words, it returns the block number of the block we are currently building. committed_block_number returns the block number of the last committed block, i.e. the block number that gets used to execute the private part of transactions when your PXE is successfully synced to the tip of the chain.
+ `TestEnvironment::pending_block_number()`
+ `TestEnvironment::committed_block_number()`
[aztec-nr] compute_nullifier_without_context renamed
The compute_nullifier_without_context function from NoteHash (ex NoteInterface) is now called compute_nullifier_unconstrained, and instead of taking storage slot, contract address and nonce it takes a note hash for nullification (same as compute_note_hash). This makes writing this
function simpler:
- unconstrained fn compute_nullifier_without_context(self, storage_slot: Field, contract_address: AztecAddress, nonce: Field) -> Field {
- let note_hash_for_nullify = ...;
+ unconstrained fn compute_nullifier_unconstrained(self, note_hash_for_nullify: Field) -> Field {
...
}
U128 type replaced with native u128
The U128 type has been replaced with the native u128 type. This means that you can no longer use the U128 type in your code. Instead, you should use the u128 type.
Doing the changes is as straightforward as:
#[public]
#[view]
- fn balance_of_public(owner: AztecAddress) -> U128 {
+ fn balance_of_public(owner: AztecAddress) -> u128 {
storage.public_balances.at(owner).read()
}
UintNote has also been updated to use the native u128 type.
[aztec-nr] Removed compute_note_hash_and_optionally_a_nullifier
This function is no longer mandatory for contracts, and the #[aztec] macro no longer injects it.
[PXE] Removed addNote and addNullifiedNote
These functions have been removed from PXE and the base Wallet interface. If you need to deliver a note manually because its creation is not being broadcast in an encrypted log, then create an unconstrained contract function to process it and simulate execution of it. The aztec::discovery::private_logs::do_process_log function can be used to perform note discovery and add to it to PXE.
See an example of how to handle a TransparentNote:
unconstrained fn deliver_transparent_note(
contract_address: AztecAddress,
amount: Field,
secret_hash: Field,
tx_hash: Field,
unique_note_hashes_in_tx: BoundedVec<Field, MAX_NOTE_HASHES_PER_TX>,
first_nullifier_in_tx: Field,
recipient: AztecAddress,
) {
// do_process_log expects a standard aztec-nr encoded note, which has the following shape:
// [ storage_slot, note_type_id, ...packed_note ]
let note = TransparentNote::new(amount, secret_hash);
let log_plaintext = BoundedVec::from_array(array_concat(
[
MyContract::storage_layout().my_state_variable.slot,
TransparentNote::get_note_type_id(),
],
note.pack(),
));
do_process_log(
contract_address,
log_plaintext,
tx_hash,
unique_note_hashes_in_tx,
first_nullifier_in_tx,
recipient,
_compute_note_hash_and_nullifier,
);
}
The note is then processed by calling this function:
const txEffects = await wallet.getTxEffect(txHash);
await contract.methods
.deliver_transparent_note(
contract.address,
new Fr(amount),
secretHash,
txHash.hash,
toBoundedVec(txEffects!.data.noteHashes, MAX_NOTE_HASHES_PER_TX),
txEffects!.data.nullifiers[0],
wallet.getAddress(),
)
.simulate();
Fee is mandatory
All transactions must now pay fees. Previously, the default payment method was NoFeePaymentMethod; It has been changed to FeeJuicePaymentMethod, with the wallet owner as the fee payer.
For example, the following code will still work:
await TokenContract.at(address, wallet).methods.transfer(recipient, 100n).send().wait();
However, the wallet owner must have enough fee juice to cover the transaction fee. Otherwise, the transaction will be rejected.
The 3 test accounts deployed in the sandbox are pre-funded with 10 ^ 22 fee juice, allowing them to send transactions right away.
In addition to the native fee juice, users can pay the transaction fees using tokens that have a corresponding FPC contract. The sandbox now includes BananaCoin and BananaFPC. Users can use a funded test account to mint banana coin for a new account. The new account can then start sending transactions and pay fees with banana coin.
import { getDeployedTestAccountsWallets } from "@aztec/accounts/testing";
import {
getDeployedBananaCoinAddress,
getDeployedBananaFPCAddress,
} from "@aztec/aztec";
// Fetch the funded test accounts.
const [fundedWallet] = await getDeployedTestAccountsWallets(pxe);
// Create a new account.
const secret = Fr.random();
const signingKey = GrumpkinScalar.random();
const alice = await getSchnorrAccount(pxe, secret, signingKey);
const aliceWallet = await alice.getWallet();
const aliceAddress = alice.getAddress();
// Deploy the new account using the pre-funded test account.
await alice.deploy({ deployWallet: fundedWallet }).wait();
// Mint banana coin for the new account.
const bananaCoinAddress = await getDeployedBananaCoinAddress(pxe);
const bananaCoin = await TokenContract.at(bananaCoinAddress, fundedWallet);
const mintAmount = 10n ** 20n;
await bananaCoin.methods
.mint_to_private(fundedWallet.getAddress(), aliceAddress, mintAmount)
.send()
.wait();
// Use the new account to send a tx and pay with banana coin.
const transferAmount = 100n;
const bananaFPCAddress = await getDeployedBananaFPCAddress(pxe);
const paymentMethod = new PrivateFeePaymentMethod(
bananaFPCAddress,
aliceWallet,
);
const receipt = await bananaCoin
.withWallet(aliceWallet)
.methods.transfer(recipient, transferAmount)
.send({ fee: { paymentMethod } })
.wait();
const transactionFee = receipt.transactionFee!;
// Check the new account's balance.
const aliceBalance = await bananaCoin.methods
.balance_of_private(aliceAddress)
.simulate();
expect(aliceBalance).toEqual(mintAmount - transferAmount - transactionFee);
The tree of protocol contract addresses is now an indexed tree
This is to allow for non-membership proofs for non-protocol contract addresses. As before, the canonical protocol contract addresses point to the index of the leaf of the 'real' computed protocol address.
For example, the canonical DEPLOYER_CONTRACT_ADDRESS is a constant = 2. This is used in the kernels as the contract_address. We calculate the computed_address (currently 0x1665c5fbc1e58ba19c82f64c0402d29e8bbf94b1fde1a056280d081c15b0dac1) and check that this value exists in the indexed tree at index 2. This check already existed and ensures that the call cannot do 'special' protocol contract things unless it is a real protocol contract.
The new check an indexed tree allows is non-membership of addresses of non protocol contracts. This ensures that if a call is from a protocol contract, it must use the canonical address. For example, before this check a call could be from the deployer contract and use 0x1665c5fbc1e58ba19c82f64c0402d29e8bbf94b1fde1a056280d081c15b0dac1 as the contract_address, but be incorrectly treated as a 'normal' call.
- let computed_protocol_contract_tree_root = if is_protocol_contract {
- 0
- } else {
- root_from_sibling_path(
- computed_address.to_field(),
- protocol_contract_index,
- private_call_data.protocol_contract_sibling_path,
- )
- };
+ conditionally_assert_check_membership(
+ computed_address.to_field(),
+ is_protocol_contract,
+ private_call_data.protocol_contract_leaf,
+ private_call_data.protocol_contract_membership_witness,
+ protocol_contract_tree_root,
+ );
[Aztec.nr] Changes to note interfaces and note macros
In this releases we decided to do a large refactor of notes which resulted in the following changes:
- We removed
NoteHeaderand we've introduced aHintedNotestruct that contains a note and the information originally stored in theNoteHeader. - We removed the
pack_contentandunpack_contentfunctions from theNoteInterfaceand made notes implement the standardPackabletrait. - We renamed the
NullifiableNotetrait toNoteHashand we've moved thecompute_note_hashfunction to this trait from theNoteInterfacetrait. - We renamed
NoteInterfacetrait asNoteTypeandget_note_type_idfunction asget_id. - The
#[note]and#[partial_note]macros now generate both theNoteTypeandNoteHashtraits. #[custom_note_interface]macro has been renamed to#[custom_note]and it now implements theNoteInterfacetrait.
This led us to do the following changes to the interfaces:
-pub trait NoteInterface<let N: u32> {
+pub trait NoteType {
fn get_id() -> Field;
- fn pack_content(self) -> [Field; N];
- fn unpack_content(fields: [Field; N]) -> Self;
- fn get_header(self) -> NoteHeader;
- fn set_header(&mut self, header: NoteHeader) -> ();
- fn compute_note_hash(self) -> Field;
}
pub trait NoteHash {
+ fn compute_note_hash(self, storage_slot: Field) -> Field;
fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field;
- unconstrained fn compute_nullifier_without_context(self) -> Field;
+ unconstrained fn fn compute_nullifier_without_context(self, storage_slot: Field, contract_address: AztecAddress, note_nonce: Field) -> Field;
}
If you are using #[note] or #[partial_note(...)] macros you will need to delete the implementations of the NullifiableNote (now NoteHash) trait as it now gets auto-generated.
Your note will also need to have an owner (a note struct field called owner) as its used in the auto-generated nullifier functions.
If you need a custom implementation of the NoteHash interface use the #[custom_note] macro.
If you used #[note_custom_interface] macro before you will need to update your notes by using the #[custom_note] macro and implementing the compute_note_hash function.
If you have no need for a custom implementation of the compute_note_hash function copy the default one:
fn compute_note_hash(self, storage_slot: Field) -> Field {
let inputs = aztec::protocol::utils::arrays::array_concat(self.pack(), [storage_slot]);
aztec::protocol::hash::poseidon2_hash_with_separator(inputs, aztec::protocol::constants::DOM_SEP__NOTE_HASH)
}
If you need to keep the custom implementation of the packing functionality, manually implement the Packable trait:
+ use dep::aztec::protocol::traits::Packable;
+impl Packable<N> for YourNote {
+ fn pack(self) -> [Field; N] {
+ ...
+ }
+
+ fn unpack(fields: [Field; N]) -> Self {
+ ...
+ }
+}
If you don't provide a custom implementation of the Packable trait, a default one will be generated.
[Aztec.nr] Changes to state variables
Since we've removed NoteHeader from notes we no longer need to modify the header in the notes when working with state variables.
This means that we no longer need to be passing a mutable note reference which led to the following changes in the API.
PrivateImmutable
For PrivateImmutable the changes are fairly straightforward.
Instead of passing in a mutable reference &mut note just pass in note.
impl<Note> PrivateImmutable<Note, &mut PrivateContext> {
- pub fn initialize<let N: u32>(self, note: &mut Note) -> NoteEmission<Note>
+ pub fn initialize<let N: u32>(self, note: Note) -> NoteEmission<Note>
where
Note: NoteInterface<N> + NullifiableNote,
{
...
}
}
PrivateSet
For PrivateSet the changes are a bit more involved than the changes in PrivateImmutable.
Instead of passing in a mutable reference &mut note to the insert function just pass in note.
The remove function now takes in a HintedNote<Note> instead of a Note and the get_notes function
now returns a vector HintedNotes instead of a vector Notes.
Note getters now generally return HintedNotes so getting a hold of the HintedNote for removal should be straightforward.
impl<Note, let N: u32> PrivateSet<Note, &mut PrivateContext>
where
Note: NoteInterface<N> + NullifiableNote + Eq,
{
- pub fn insert(self, note: &mut Note) -> NoteEmission<Note> {
+ pub fn insert(self, note: Note) -> NoteEmission<Note> {
...
}
- pub fn remove(self, note: Note) {
+ pub fn remove(self, hinted_note: HintedNote<Note>) {
...
}
pub fn get_notes<PREPROCESSOR_ARGS, FILTER_ARGS>(
self,
options: NoteGetterOptions<Note, N, PREPROCESSOR_ARGS, FILTER_ARGS>,
- ) -> BoundedVec<Note, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL> {
+ ) -> BoundedVec<HintedNote<Note>, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL> {
...
}
}
- impl<Note, let N: u32> PrivateSet<Note, &mut PublicContext>
- where
- Note: NoteInterface<N> + NullifiableNote,
- {
- pub fn insert_from_public(self, note: &mut Note) {
- create_note_hash_from_public(self.context, self.storage_slot, note);
- }
- }
PrivateMutable
For PrivateMutable the changes are similar to the changes in PrivateImmutable.
impl<Note, let N: u32> PrivateMutable<Note, &mut PrivateContext>
where
Note: NoteInterface<N> + NullifiableNote,
{
- pub fn initialize(self, note: &mut Note) -> NoteEmission<Note> {
+ pub fn initialize(self, note: Note) -> NoteEmission<Note> {
...
}
- pub fn replace(self, new_note: &mut Note) -> NoteEmission<Note> {
+ pub fn replace(self, new_note: Note) -> NoteEmission<Note> {
...
}
- pub fn initialize_or_replace(self, note: &mut Note) -> NoteEmission<Note> {
+ pub fn initialize_or_replace(self, note: Note) -> NoteEmission<Note> {
...
}
}
0.75.0
Changes to TokenBridge interface
get_token and get_portal_address functions got merged into a single get_config function that returns a struct containing both the token and portal addresses.
[Aztec.nr] SharedMutable can store size of packed length larger than 1
SharedMutable has been modified such that now it can store type T which packs to a length larger than 1.
This is a breaking change because now SharedMutable requires T to implement Packable trait instead of ToField and FromField traits.
To implement the Packable trait for your type you can use the derive macro:
+ use std::meta::derive;
+ #[derive(Packable)]
pub struct YourType {
...
}
[Aztec.nr] Introduction of WithHash<T>
WithHash<T> is a struct that allows for efficient reading of value T from public storage in private.
This is achieved by storing the value with its hash, then obtaining the values via an oracle and verifying them against the hash.
This results in in a fewer tree inclusion proofs for values T that are packed into more than a single field.
WithHash<T> is leveraged by state variables like PublicImmutable.
This is a breaking change because now we require values stored in PublicImmutable and SharedMutable to implement the Eq trait.
To implement the Eq trait you can use the #[derive(Eq)] macro:
+ use std::meta::derive;
+ #[derive(Eq)]
pub struct YourType {
...
}
0.73.0
[Token, FPC] Moving fee-related complexity from the Token to the FPC
There was a complexity leak of fee-related functionality in the token contract.
We've came up with a way how to achieve the same objective with the general functionality of the Token contract.
This lead to the removal of setup_refund and complete_refund functions from the Token contract and addition of complete_refund function to the FPC.
[Aztec.nr] Improved storage slot allocation
State variables are no longer assumed to be generic over a type that implements the Serialize trait: instead, they must implement the Storage trait with an N value equal to the number of slots they need to reserve.
For the vast majority of state variables, this simply means binding the serialization length to this trait:
+ impl<T, let N: u32> Storage<N> for MyStateVar<T> where T: Serialize<N> { };