Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
11 changes: 10 additions & 1 deletion .github/workflows/aztec-cli-acceptance-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,18 @@ 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'
if: (success() || failure()) && github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
env:
VERSION: ${{ github.event.inputs.version || github.event.workflow_run.head_branch }}
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
10 changes: 10 additions & 0 deletions ci3/release_prep_package_json
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ for deps in dependencies devDependencies peerDependencies; do
mv tmp.json package.json
done
done

# Stamp aztec_version into any published noir contract artifacts in artifacts/*.json. The build-time stamp in
# noir-projects/noir-contracts/bootstrap.sh writes "dev"; $version here is the authoritative release version about to
# be written to package.json. We cannot stamp real version in bootstrap because there we don't have access to it.
if [ -d artifacts ]; then
for f in artifacts/*.json; do
[ -e "$f" ] || continue
jq --arg v "$version" '.aztec_version = $v' "$f" >$tmp && mv $tmp "$f"
done
fi
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
88 changes: 81 additions & 7 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,76 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [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).

### [Protocol] Public-key hashes replace points in `PublicKeys`

Ships together with immutables hash changes (shown below).
Expand Down Expand Up @@ -52,7 +122,6 @@ The same field-rename applies to `.ovpk_m` and `.tpk_m`: these are now `.ovpk_m_

**Security note (PXE side).** The kernel circuit no longer checks that `npk_m`, `ovpk_m`, `tpk_m` are on-curve or non-infinity (those points are no longer in the witness). The PXE / key store relies on `deriveKeys`'s by-construction guarantee that derived points are on-curve and non-infinity. Account-creation flows that bypass `deriveKeys` (e.g. importing pre-derived public keys from an external source) must validate this themselves, or risk producing unspendable notes.


### [Contracts] `ContractInstance` gains `immutablesHash`, address derivation changes

`ContractInstance` now has a new `immutablesHash: Fr` field that commits to a contract's immutable storage values. The field is folded into the salted initialization hash, so contract addresses are impacted:
Expand Down Expand Up @@ -86,7 +155,6 @@ salted_initialization_hash = poseidon2(DOM_SEP__SALTED_INITIALIZATION_HASH, [sal

The `aztec.js` `publishInstance` helper handles this automatically.


### [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 @@ -180,7 +248,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 @@ -333,7 +401,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 @@ -350,7 +424,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 @@ -477,8 +551,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 @@ -861,7 +862,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
Loading
Loading