Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/aztec-cli-acceptance-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ jobs:
timeout-minutes: 30
run: ./aztec-up/test/aztec-cli-acceptance-test/run-test.sh

- name: Upload diagnostics on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: acceptance-test-diagnostics-${{ matrix.os }}
path: ${{ runner.temp }}/aztec-cli-acceptance-test-*/**
if-no-files-found: warn
retention-days: 7

notify:
needs: release-acceptance
if: always() && github.event_name != 'workflow_dispatch'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ if (!existsSync(join(AZTEC_INSTALL_DIR, "package.json"))) {
process.exit(2);
}

const TMP_DIR = mkdtempSync(join(tmpdir(), "aztec-cli-acceptance-test-"));
// Prefer RUNNER_TEMP so the GitHub Actions upload-artifact step can find the diagnostic
// tree on failure under a predictable parent path. Falls back to the system tmpdir locally.
const TMP_DIR_PARENT = process.env.RUNNER_TEMP ?? tmpdir();
const TMP_DIR = mkdtempSync(join(TMP_DIR_PARENT, "aztec-cli-acceptance-test-"));
const WORKSPACE_DIR = join(TMP_DIR, "my_workspace");

// Exit codes follow the Unix 128+signal convention for signal terminations.
Expand Down Expand Up @@ -188,13 +191,32 @@ function locateArtifact(): string {
async function startLocalNetwork(): Promise<void> {
const logPath = join(TMP_DIR, "local_network.log");
const logFd = openSync(logPath, "a");
// LOG_LEVEL defaults to "debug" so failed CI runs leave useful traces in local_network.log;
// override with LOCAL_NETWORK_LOG_LEVEL=silent when running locally and the volume is noisy.
const logLevel = process.env.LOCAL_NETWORK_LOG_LEVEL ?? "debug";
const reportDir = join(TMP_DIR, "node-reports");
mkdirSync(reportDir, { recursive: true });
const nodeOptions = [
process.env.NODE_OPTIONS,
`--report-on-signal`,
`--report-directory=${reportDir}`,
]
.filter(Boolean)
.join(" ");
const proc = spawn("aztec", ["start", "--local-network"], {
cwd: TMP_DIR,
stdio: ["ignore", logFd, logFd],
env: { ...process.env, LOG_LEVEL: "silent", PXE_PROVER: "none" },
env: {
...process.env,
LOG_LEVEL: logLevel,
PXE_PROVER: "none",
NODE_OPTIONS: nodeOptions,
},
});
closeSync(logFd);
log(` local-network pid=${proc.pid}, log=${logPath}`);
log(
` local-network pid=${proc.pid}, log=${logPath}, LOG_LEVEL=${logLevel}`,
);

// Kill the network on process exit (including SIGINT/SIGTERM via the signal handlers).
process.on("exit", () => {
Expand All @@ -214,6 +236,10 @@ async function startLocalNetwork(): Promise<void> {
);
}
if (Date.now() > deadline) {
try {
process.kill(proc.pid!, "SIGUSR2");
await delay(2000);
} catch {}
dumpTail(logPath);
fail(
`timed out after ${msToSecs(LOCAL_NETWORK_READY_TIMEOUT_MS)}s waiting for local-network /status (see ${logPath})`,
Expand Down Expand Up @@ -306,7 +332,7 @@ function leaveTmpDirForInspection() {
console.error(`>>> Left tmp dir at ${TMP_DIR} for inspection`);
}

function dumpTail(path: string, lines = 100) {
function dumpTail(path: string, lines = 400) {
if (!existsSync(path)) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/docs-developers/docs/aztec-nr/standards/aip-721.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl NFTNote {
// ... creates encrypted log and validity commitment
let partial_note = PartialNFTNote { commitment };
let validity_commitment = partial_note.compute_validity_commitment(completer);
context.push_nullifier(validity_commitment);
context.push_nullifier_unsafe(validity_commitment);
partial_note
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ You can cancel an authwit before it's used by emitting its nullifier directly. T
fn cancel_authwit(inner_hash: Field) {
let on_behalf_of = self.msg_sender();
let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
self.context.push_nullifier(nullifier);
self.context.push_nullifier_unsafe(nullifier);
}
```

Expand Down
97 changes: 92 additions & 5 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,87 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.js] `AccountManager.create` takes an options bag

`AccountManager.create` no longer takes `salt` as a positional argument. The trailing `salt?: Salt` parameter has been folded into a new `AccountManagerCreateOptions` bag alongside `immutablesHash` and `deployer`:

```diff
- AccountManager.create(wallet, secret, accountContract, salt)
+ AccountManager.create(wallet, secret, accountContract, { salt })
```

`immutablesHash` lets callers commit a non-zero immutables hash on the resulting `ContractInstance` (folded into the salted initialization hash, so it affects the derived address). `deployer` overrides the deployer address recorded on the instance (defaults to `AztecAddress.ZERO`). The same `immutablesHash` field is now also threaded through `DeployMethod` / `DeployAccountMethod` so the address derived at deploy time matches the one on `accountManager.getInstance()`.

### [Aztec.nr] Defining a custom `sync_state` function now requires `AztecConfig`

Contracts that previously overrode the default `sync_state` by defining their own function with that name will now get a compile error. Use `AztecConfig::custom_sync_state()` instead.

The custom hook receives the same parameters as `do_sync_state` and is responsible for calling it if default behavior is also desired. You can perform work before and/or after the default `do_sync_state` call, or skip it entirely.

```diff
+ unconstrained fn my_custom_sync(
+ contract_address: AztecAddress,
+ compute_note_hash: ComputeNoteHash,
+ compute_note_nullifier: ComputeNoteNullifier,
+ process_custom_message: Option<CustomMessageHandler>,
+ offchain_inbox_sync: Option<OffchainInboxSync>,
+ scope: AztecAddress,
+ ) {
+ // optional: work before default sync
+ do_sync_state(contract_address, compute_note_hash, compute_note_nullifier, process_custom_message, offchain_inbox_sync, scope);
+ // optional: work after default sync
+ }

- #[aztec]
+ #[aztec(::aztec::macros::AztecConfig::new().custom_sync_state(crate::my_custom_sync))]
contract MyContract {
- use aztec::macros::functions::external;
-
- #[external("utility")]
- unconstrained fn sync_state(scope: AztecAddress) {
- // custom sync logic
- }
}
```

**Impact**: Only contracts that manually defined a `sync_state` function are affected. Contracts using the default macro-generated `sync_state` require no changes.

### [Aztec.nr] `push_nullifier` renamed to `push_nullifier_unsafe`

`PrivateContext::push_nullifier` and `PublicContext::push_nullifier` have been renamed to `push_nullifier_unsafe` to
make it clear that they are low-level functions that require careful domain separation. This is consistent with the
`_unsafe` suffix already used by `emit_private_log_unsafe`, `emit_raw_note_log_unsafe`, and `emit_public_log_unsafe`.

```diff
- context.push_nullifier(nullifier);
+ context.push_nullifier_unsafe(nullifier);
```

Prefer higher-level abstractions like `SingleUseClaim` or `destroy_note` which handle domain separation automatically.

### [Aztec.nr] `LogRetrievalRequest` now includes `source`, `from_block`, and `to_block` fields

`LogRetrievalRequest` has been extended with three new fields to support filtering logs by source and block range. The `get_logs_by_tag` oracle now also returns all matching logs per tag instead of only the first match.

A `LogRetrievalRequest::new(contract_address, tag)` constructor is provided that defaults to querying both public and private logs with no block range filter:

```rust
LogRetrievalRequest::new(contract_address, my_tag)
```

If you need to customize source or block range, construct the struct manually with the new fields:

```diff
LogRetrievalRequest {
tag: my_tag,
+ source: LogSource.PUBLIC_AND_PRIVATE,
+ from_block: Option::none(),
+ to_block: Option::none(),
}
```

`source` controls which RPCs are queried: `LogSource.PRIVATE`, `LogSource.PUBLIC`, or `LogSource.PUBLIC_AND_PRIVATE`. `from_block` and `to_block` define a half-open `[from, to)` block range filter. Both are `Option<Field>` and default to `Option::none()` (no filtering).

### [Aztec.nr] `emit_private_log_unsafe` / `emit_raw_note_log_unsafe` now take `BoundedVec`

The old array-based `emit_private_log_unsafe(tag, log: [Field; N], length)` and `emit_raw_note_log_unsafe(tag, log: [Field; N], length, note_hash_counter)` have been removed. The temporary `_vec_unsafe` variants introduced in a prior release have been renamed to take their place.
Expand Down Expand Up @@ -102,7 +183,7 @@ The `Schnorr` TypeScript API in `@aztec/foundation/crypto/schnorr` keeps the sam
+ env.call_public_incognito(SampleContract::at(addr).some_function());
```

If you need to call a public function *with* a sender, use `call_public` instead.
If you need to call a public function _with_ a sender, use `call_public` instead.

### [Aztec.nr] TXE `view_public_incognito` is deprecated

Expand Down Expand Up @@ -255,7 +336,13 @@ If you set `Noir: Nargo Path` in the VS Code Noir extension to `$HOME/.aztec/cur
```typescript
const result = await contract.methods.read_balance(account).simulate({
overrides: {
publicStorage: [{ contract: contract.address, slot: BALANCE_SLOT, value: new Fr(1_000_000n) }],
publicStorage: [
{
contract: contract.address,
slot: BALANCE_SLOT,
value: new Fr(1_000_000n),
},
],
},
});
```
Expand All @@ -272,7 +359,7 @@ Direct callers of the `SimulationOverrides` constructor must switch from a posit
`overrides.contracts` swaps contract instances in the simulator's contract DB — useful for simulating a contract being on a different class than the one it was deployed with. To simulate a complete onchain upgrade flow, use the `fastForwardContractUpdate` helper which returns a `SimulationOverrides` covering both registry storage rewrites and the upgraded instance entry:

```typescript
import { fastForwardContractUpdate } from '@aztec/aztec.js';
import { fastForwardContractUpdate } from "@aztec/aztec.js";

const overrides = await fastForwardContractUpdate({
instanceAddress: contract.address,
Expand Down Expand Up @@ -399,8 +486,8 @@ If you want the latest L1-confirmed checkpoint regardless of proposed state, swi

```ts
// Throws BadRequestError when a proposed entry exists at the resolved number:
await node.getCheckpoint('proposed', { includeAttestations: true });
await node.getCheckpoint('proposed', { includeL1PublishInfo: true });
await node.getCheckpoint("proposed", { includeAttestations: true });
await node.getCheckpoint("proposed", { includeL1PublishInfo: true });

// And when a by-number / by-slot lookup falls back to a proposed entry:
await node.getCheckpoint({ number: N }, { includeAttestations: true });
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/authwit/account.nr
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl AccountActions<&mut PrivateContext> {

if cancellable {
let tx_nullifier = poseidon2_hash_with_separator([app_payload.tx_nonce], DOM_SEP__TX_NULLIFIER);
self.context.push_nullifier(tx_nullifier);
self.context.push_nullifier_unsafe(tx_nullifier);
}
}

Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/authwit/auth.nr
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ pub fn assert_inner_hash_valid_authwit(context: &mut PrivateContext, on_behalf_o
// already be handled in the verification, so we just need something to nullify, that allows the same inner_hash
// for multiple actors.
let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
context.push_nullifier(nullifier);
context.push_nullifier_unsafe(nullifier);
}

/// Assert that `on_behalf_of` has authorized the current call in the authentication registry
Expand Down
15 changes: 8 additions & 7 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -366,16 +366,17 @@ impl PrivateContext {
/// The raw `nullifier` is not what is inserted into the Aztec state tree: it will be first siloed by contract
/// address via [`crate::protocol::hash::compute_siloed_nullifier`] in order to prevent accidental or malicious
/// interference of nullifiers from different contracts.
pub fn push_nullifier(&mut self, nullifier: Field) {
pub fn push_nullifier_unsafe(&mut self, nullifier: Field) {
notify_created_nullifier(nullifier);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: 0 }.count(self.next_counter()));
}

/// Creates a new [nullifier](crate::nullifier) associated with a note.
///
/// This is a variant of [`PrivateContext::push_nullifier`] that is used for note nullifiers, i.e. nullifiers that
/// correspond to a note. If a note and its nullifier are created in the same transaction, then the private kernels
/// will 'squash' these values, deleting them both as if they never existed and reducing transaction fees.
/// This is a variant of [`PrivateContext::push_nullifier_unsafe`] that is used for note nullifiers, i.e.
/// nullifiers that correspond to a note. If a note and its nullifier are created in the same transaction, then
/// the private kernels will 'squash' these values, deleting them both as if they never existed and reducing
/// transaction fees.
///
/// The `nullification_note_hash` must be the result of calling
/// [`crate::note::utils::compute_confirmed_note_hash_for_nullification`] for pending notes, and `0` for settled
Expand All @@ -386,10 +387,10 @@ impl PrivateContext {
/// This is a low-level function that must be used with great care to avoid subtle corruption of contract state.
/// Instead of calling this function, consider using the higher-level [`crate::note::lifecycle::destroy_note`].
///
/// The precautions listed for [`PrivateContext::push_nullifier`] apply here as well, and callers should
/// The precautions listed for [`PrivateContext::push_nullifier_unsafe`] apply here as well, and callers should
/// additionally ensure `nullification_note_hash` corresponds to a note emitted by this contract, with its hash
/// computed in the same transaction execution phase as the call to this function. Finally, only this function
/// should be used for note nullifiers, never [`PrivateContext::push_nullifier`].
/// should be used for note nullifiers, never [`PrivateContext::push_nullifier_unsafe`].
///
/// Failure to do these things can result in unprovable contexts, accidental deletion of notes, or double-spend
/// attacks.
Expand Down Expand Up @@ -858,7 +859,7 @@ impl PrivateContext {
);

// Push nullifier (and the "commitment" corresponding to this can be "empty")
self.push_nullifier(nullifier)
self.push_nullifier_unsafe(nullifier)
}

/// Emits a private log (an array of Fields) that will be published to an Ethereum blob.
Expand Down
4 changes: 2 additions & 2 deletions noir-projects/aztec-nr/aztec/src/context/public_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ impl PublicContext {
assert(!self.nullifier_exists_unsafe(nullifier, self.this_address()), "L1-to-L2 message is already nullified");
assert(self.l1_to_l2_msg_exists(message_hash, leaf_index), "Tried to consume nonexistent L1-to-L2 message");

self.push_nullifier(nullifier);
self.push_nullifier_unsafe(nullifier);
}

/// Sends an "L2 -> L1 message" from this function (Aztec, L2) to a smart contract on Ethereum (L1). L1 contracts
Expand Down Expand Up @@ -406,7 +406,7 @@ impl PublicContext {
/// The raw `nullifier` is not what is inserted into the Aztec state tree: it will be first siloed by contract
/// address via [`crate::protocol::hash::compute_siloed_nullifier`] in order to prevent accidental or malicious
/// interference of nullifiers from different contracts.
pub fn push_nullifier(_self: Self, nullifier: Field) {
pub fn push_nullifier_unsafe(_self: Self, nullifier: Field) {
// Safety: AVM opcodes are constrained by the AVM itself
unsafe { avm::emit_nullifier(nullifier) };
}
Expand Down
29 changes: 29 additions & 0 deletions noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::oracle::ephemeral;
use crate::protocol::traits::{Deserialize, Serialize};
use crate::protocol::utils::{reader::Reader, writer::Writer};

/// A dynamically sized array that exists only during a single contract call frame.
///
Expand Down Expand Up @@ -101,6 +102,34 @@ impl<T> EphemeralArray<T> {
}
}

/// Serializes an `EphemeralArray` as its slot identifier, allowing oracle function signatures to use
/// `EphemeralArray<T>` instead of opaque `Field` slots.
impl<T> Serialize for EphemeralArray<T> {
let N: u32 = 1;

fn serialize(self) -> [Field; Self::N] {
[self.slot]
}

fn stream_serialize<let K: u32>(self, writer: &mut Writer<K>) {
writer.write(self.slot);
}
}

/// Deserializes a single Field into an `EphemeralArray` handle, treating the field value as the slot identifier.
/// This is the inverse of [`Serialize`].
impl<T> Deserialize for EphemeralArray<T> {
let N: u32 = 1;

fn deserialize(fields: [Field; Self::N]) -> Self {
Self { slot: fields[0] }
}

fn stream_deserialize<let K: u32>(reader: &mut Reader<K>) -> Self {
Self { slot: reader.read() }
}
}

mod test {
use crate::test::helpers::test_environment::TestEnvironment;
use crate::test::mocks::MockStruct;
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/event/event_emission.nr
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ where
// The event commitment is emitted as a nullifier instead of as a note because these are simpler: nullifiers cannot
// be squashed, making kernel processing simpler, and they have no nonce that recipients need to discover.
let commitment = compute_private_event_commitment(event, randomness);
context.push_nullifier(commitment);
context.push_nullifier_unsafe(commitment);

EventMessage::new(NewEvent { event, randomness }, context)
}
Expand Down
14 changes: 8 additions & 6 deletions noir-projects/aztec-nr/aztec/src/history/nullifier.nr
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ mod test;
/// Asserts that a nullifier existed by the time a block was mined.
///
/// This function takes a _siloed_ nullifier, i.e. the value that is actually stored in the tree. Use
/// [`crate::protocol::hash::compute_siloed_nullifier`] to convert an inner nullifier (what
/// [`PrivateContext::push_nullifier`](crate::context::PrivateContext::push_nullifier) takes) into a siloed one.
/// [`crate::protocol::hash::compute_siloed_nullifier`] to convert an inner nullifier
/// (what [`PrivateContext::push_nullifier_unsafe`](crate::context::PrivateContext::push_nullifier_unsafe)
/// takes) into a siloed one.
///
/// Note that this does not mean that the nullifier was created **at** `block_header`, only that it was present in the
/// tree once all transactions from `block_header` were executed.
Expand Down Expand Up @@ -56,8 +57,9 @@ pub fn assert_nullifier_existed_by(block_header: BlockHeader, siloed_nullifier:
/// Asserts that a nullifier did not exist by the time a block was mined.
///
/// This function takes a _siloed_ nullifier, i.e. the value that is actually stored in the tree. Use
/// [`crate::protocol::hash::compute_siloed_nullifier`] to convert an inner nullifier (what
/// [`PrivateContext::push_nullifier`](crate::context::PrivateContext::push_nullifier) takes) into a siloed one.
/// [`crate::protocol::hash::compute_siloed_nullifier`] to convert an inner nullifier
/// (what [`PrivateContext::push_nullifier_unsafe`](crate::context::PrivateContext::push_nullifier_unsafe)
/// takes) into a siloed one.
///
/// In order to prove that a nullifier **did** exist by `block_header`, use [`assert_nullifier_existed_by`].
///
Expand All @@ -69,8 +71,8 @@ pub fn assert_nullifier_existed_by(block_header: BlockHeader, siloed_nullifier:
///
/// If you **must** prove that a nullifier does not exist by the time a transaction is executed, there only two ways to
/// do this: by actually emitting the nullifier via
/// [`PrivateContext::push_nullifier`](crate::context::PrivateContext::push_nullifier) (which can of course can be done
/// once), or by calling a public contract function that calls
/// [`PrivateContext::push_nullifier_unsafe`](crate::context::PrivateContext::push_nullifier_unsafe)
/// (which can of course only be done once), or by calling a public contract function that calls
/// [`PublicContext::nullifier_exists_unsafe`](crate::context::PublicContext::nullifier_exists_unsafe) (which leaks
/// that this nullifier is being checked):
///
Expand Down
Loading
Loading