From d023d3fa8db45bc669bfc6099e64258d01f0bd95 Mon Sep 17 00:00:00 2001
From: Aztec Bot <49558828+AztecBot@users.noreply.github.com>
Date: Thu, 19 Mar 2026 11:02:57 -0400
Subject: [PATCH 01/10] chore: backport #21754 (feat!: make
isContractInitialized a tri-state enum) to v4-next (#21792)
---
.../docs/aztec-js/how_to_create_account.md | 2 +-
.../docs/aztec-js/how_to_deploy_contract.md | 2 +-
.../docs/aztec-js/how_to_deploy_contract.md | 2 +-
.../docs/resources/migration_notes.md | 23 ++++++-
docs/examples/ts/aztecjs_connection/index.ts | 2 +-
.../navbar/components/WalletHub.tsx | 6 +-
yarn-project/aztec.js/src/api/wallet.ts | 1 +
.../aztec.js/src/wallet/wallet.test.ts | 6 +-
yarn-project/aztec.js/src/wallet/wallet.ts | 22 +++++--
yarn-project/bot/src/factory.ts | 3 +-
.../end-to-end/src/e2e_block_building.test.ts | 5 +-
.../e2e_deploy_contract/deploy_method.test.ts | 17 +++++-
.../private_initialization.test.ts | 27 ++++++++
.../wallet-sdk/src/base-wallet/base_wallet.ts | 61 +++++++++++--------
14 files changed, 131 insertions(+), 48 deletions(-)
diff --git a/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_create_account.md b/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_create_account.md
index 626a82ad921e..bc1ee3bdc337 100644
--- a/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_create_account.md
+++ b/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_create_account.md
@@ -103,7 +103,7 @@ Confirm the account was deployed successfully:
```typescript title="verify_account_deployment" showLineNumbers
const metadata = await wallet.getContractMetadata(newAccount.address);
-console.log("Account deployed:", metadata.isContractInitialized);
+console.log("Account deployed:", metadata.initializationStatus);
```
> Source code: docs/examples/ts/aztecjs_connection/index.ts#L86-L89
diff --git a/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_deploy_contract.md b/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_deploy_contract.md
index f3972921bd73..bfc25a8682e3 100644
--- a/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_deploy_contract.md
+++ b/docs/developer_versioned_docs/version-v4.0.0-nightly.20260217/docs/aztec-js/how_to_deploy_contract.md
@@ -366,7 +366,7 @@ const metadata = await wallet.getContractMetadata(contractAddress);
metadata.instance; // Contract registered in your wallet?
metadata.isContractClassPubliclyRegistered; // Class registered on the network?
metadata.isContractPublished; // Instance registered on the network?
-metadata.isContractInitialized; // Constructor has been called?
+metadata.initializationStatus; // Constructor has been called?
```
For a complete overview of what these states mean and when functions become callable, see [Contract Readiness States](../aztec-nr/contract_readiness_states.md).
diff --git a/docs/docs-developers/docs/aztec-js/how_to_deploy_contract.md b/docs/docs-developers/docs/aztec-js/how_to_deploy_contract.md
index 3871fa7f529f..67e3d6e9e5ed 100644
--- a/docs/docs-developers/docs/aztec-js/how_to_deploy_contract.md
+++ b/docs/docs-developers/docs/aztec-js/how_to_deploy_contract.md
@@ -168,7 +168,7 @@ const metadata = await wallet.getContractMetadata(contractAddress);
metadata.instance; // Contract registered in your wallet?
metadata.isContractClassPubliclyRegistered; // Class registered on the network?
metadata.isContractPublished; // Instance registered on the network?
-metadata.isContractInitialized; // Constructor has been called?
+metadata.initializationStatus; // Constructor has been called?
```
For a complete overview of what these states mean and when functions become callable, see [Contract Readiness States](../aztec-nr/contract_readiness_states.md).
diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md
index 2f53566279aa..0b31954fd9b9 100644
--- a/docs/docs-developers/docs/resources/migration_notes.md
+++ b/docs/docs-developers/docs/resources/migration_notes.md
@@ -9,6 +9,28 @@ Aztec is in active development. Each version may introduce breaking changes that
## TBD
+### [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 initialized
+- `UNKNOWN`: 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:**
+
+```diff
++ 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`.
@@ -64,7 +86,6 @@ The `scope` field in `ExecuteUtilityOptions` has been renamed to `scopes` and ch
```
**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`).
diff --git a/docs/examples/ts/aztecjs_connection/index.ts b/docs/examples/ts/aztecjs_connection/index.ts
index ff778ea72222..a9d7643f99ca 100644
--- a/docs/examples/ts/aztecjs_connection/index.ts
+++ b/docs/examples/ts/aztecjs_connection/index.ts
@@ -85,7 +85,7 @@ await deployMethodFeeJuice.send({
// docs:start:verify_account_deployment
const metadata = await wallet.getContractMetadata(newAccount.address);
-console.log("Account deployed:", metadata.isContractInitialized);
+console.log("Account deployed:", metadata.initializationStatus);
// docs:end:verify_account_deployment
// docs:start:deploy_contract
diff --git a/playground/src/components/navbar/components/WalletHub.tsx b/playground/src/components/navbar/components/WalletHub.tsx
index 4d4707c3604a..b214559e132e 100644
--- a/playground/src/components/navbar/components/WalletHub.tsx
+++ b/playground/src/components/navbar/components/WalletHub.tsx
@@ -36,7 +36,7 @@ import {
} from '@aztec/wallet-sdk/manager';
import { hashToEmoji } from '@aztec/wallet-sdk/crypto';
import { Fr } from '@aztec/foundation/curves/bn254';
-import type { Wallet } from '@aztec/aztec.js/wallet';
+import { ContractInitializationStatus, type Wallet } from '@aztec/aztec.js/wallet';
type ExtendedWalletProvider = Omit & {
type: WalletProvider['type'] | 'embedded';
@@ -86,8 +86,8 @@ async function discoverTestAccounts(wallet: EmbeddedWallet) {
return;
}
- const { isContractInitialized } = await wallet.getContractMetadata(sampleAccount.address);
- if (!isContractInitialized) {
+ const { initializationStatus } = await wallet.getContractMetadata(sampleAccount.address);
+ if (initializationStatus !== ContractInitializationStatus.INITIALIZED) {
return;
}
diff --git a/yarn-project/aztec.js/src/api/wallet.ts b/yarn-project/aztec.js/src/api/wallet.ts
index ae7d39b316f7..febf62ad7ac2 100644
--- a/yarn-project/aztec.js/src/api/wallet.ts
+++ b/yarn-project/aztec.js/src/api/wallet.ts
@@ -15,6 +15,7 @@ export {
type PublicEvent,
type PublicEventFilter,
type ContractMetadata,
+ ContractInitializationStatus,
type ContractClassMetadata,
AppCapabilitiesSchema,
WalletCapabilitiesSchema,
diff --git a/yarn-project/aztec.js/src/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts
index 6ef110d65cd5..e460093b75b1 100644
--- a/yarn-project/aztec.js/src/wallet/wallet.test.ts
+++ b/yarn-project/aztec.js/src/wallet/wallet.test.ts
@@ -39,7 +39,7 @@ import type {
SimulateOptions,
Wallet,
} from './wallet.js';
-import { WalletSchema } from './wallet.js';
+import { ContractInitializationStatus, WalletSchema } from './wallet.js';
describe('WalletSchema', () => {
let handler: MockWallet;
@@ -107,7 +107,7 @@ describe('WalletSchema', () => {
const result = await context.client.getContractMetadata(await AztecAddress.random());
expect(result).toEqual({
instance: undefined,
- isContractInitialized: undefined,
+ initializationStatus: ContractInitializationStatus.UNKNOWN,
isContractPublished: expect.any(Boolean),
isContractUpdated: expect.any(Boolean),
updatedContractClassId: undefined,
@@ -408,7 +408,7 @@ class MockWallet implements Wallet {
getContractMetadata(_address: AztecAddress): Promise {
return Promise.resolve({
instance: undefined,
- isContractInitialized: undefined,
+ initializationStatus: ContractInitializationStatus.UNKNOWN,
isContractPublished: false,
isContractUpdated: false,
updatedContractClassId: undefined,
diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts
index c827b5977106..efeadb6af6dd 100644
--- a/yarn-project/aztec.js/src/wallet/wallet.ts
+++ b/yarn-project/aztec.js/src/wallet/wallet.ts
@@ -203,17 +203,27 @@ export type PublicEvent = Event<
}
>;
+/** Whether the contract has been initialized. */
+export enum ContractInitializationStatus {
+ /** The contract has been initialized (initialization nullifier found). */
+ INITIALIZED = 'INITIALIZED',
+ /** The contract has not been initialized (instance is known, but no initialization nullifier found). */
+ UNINITIALIZED = 'UNINITIALIZED',
+ /**
+ * Initialization status cannot be determined. The contract instance is not registered in this wallet, so we have
+ * limited ability to check for initialization. The contract may or may not have been initialized.
+ */
+ UNKNOWN = 'UNKNOWN',
+}
+
/**
* Contract metadata including deployment and registration status.
*/
export type ContractMetadata = {
/** The contract instance */
instance?: ContractInstanceWithAddress;
- /**
- * Whether the contract has been initialized (initialization nullifier exists).
- * Undefined when instance is not registered.
- */
- isContractInitialized: boolean | undefined;
+ /** Whether the contract has been initialized. */
+ initializationStatus: ContractInitializationStatus;
/** Whether the contract instance is publicly deployed on-chain */
isContractPublished: boolean;
/** Whether the contract has been updated to a different class */
@@ -377,7 +387,7 @@ export const PublicEventSchema = zodFor>()(
export const ContractMetadataSchema = z.object({
instance: optional(ContractInstanceWithAddressSchema),
- isContractInitialized: optional(z.boolean()),
+ initializationStatus: z.nativeEnum(ContractInitializationStatus),
isContractPublished: z.boolean(),
isContractUpdated: z.boolean(),
updatedContractClassId: optional(schemas.Fr),
diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts
index efe182ad0800..e12d6534354f 100644
--- a/yarn-project/bot/src/factory.ts
+++ b/yarn-project/bot/src/factory.ts
@@ -16,6 +16,7 @@ import { deriveKeys } from '@aztec/aztec.js/keys';
import { createLogger } from '@aztec/aztec.js/log';
import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
import { waitForTx } from '@aztec/aztec.js/node';
+import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
import { createEthereumChain } from '@aztec/ethereum/chain';
import { createExtendedL1Client } from '@aztec/ethereum/client';
import { RollupContract } from '@aztec/ethereum/contracts';
@@ -207,7 +208,7 @@ export class BotFactory {
const signingKey = deriveSigningKey(secret);
const accountManager = await this.wallet.createSchnorrAccount(secret, salt, signingKey);
const metadata = await this.wallet.getContractMetadata(accountManager.address);
- if (metadata.isContractInitialized) {
+ if (metadata.initializationStatus === ContractInitializationStatus.INITIALIZED) {
this.log.info(`Account at ${accountManager.address.toString()} already initialized`);
const timer = new Timer();
const address = accountManager.address;
diff --git a/yarn-project/end-to-end/src/e2e_block_building.test.ts b/yarn-project/end-to-end/src/e2e_block_building.test.ts
index 40c58422ce4d..40b2e4ea307f 100644
--- a/yarn-project/end-to-end/src/e2e_block_building.test.ts
+++ b/yarn-project/end-to-end/src/e2e_block_building.test.ts
@@ -5,6 +5,7 @@ import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import { type AztecNode, waitForTx } from '@aztec/aztec.js/node';
import { TxStatus } from '@aztec/aztec.js/tx';
+import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
import { AnvilTestWatcher, CheatCodes } from '@aztec/aztec/testing';
import { asyncMap } from '@aztec/foundation/async-map';
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
@@ -158,9 +159,9 @@ describe('e2e_block_building', () => {
// Assert all contracts got initialized
const areInitialized = await Promise.all(
- addresses.map(async a => (await wallet.getContractMetadata(a)).isContractInitialized),
+ addresses.map(async a => (await wallet.getContractMetadata(a)).initializationStatus),
);
- expect(areInitialized).toEqual(times(TX_COUNT, () => true));
+ expect(areInitialized).toEqual(times(TX_COUNT, () => ContractInitializationStatus.INITIALIZED));
});
it('assembles a block with multiple txs with public fns', async () => {
diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts
index 0b2dbf226b20..601be528a2ca 100644
--- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts
+++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts
@@ -3,7 +3,7 @@ import { BatchCall } from '@aztec/aztec.js/contracts';
import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import { type AztecNode, createAztecNodeClient } from '@aztec/aztec.js/node';
-import type { Wallet } from '@aztec/aztec.js/wallet';
+import { ContractInitializationStatus, type Wallet } from '@aztec/aztec.js/wallet';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
import { CounterContract } from '@aztec/noir-test-contracts.js/Counter';
import { InitTestContract } from '@aztec/noir-test-contracts.js/InitTest';
@@ -201,8 +201,23 @@ describe('e2e_deploy_contract deploy method', () => {
publicCallTxPromise,
]);
expect(deployTxReceipt.blockNumber).toEqual(publicCallTxReceipt.blockNumber);
+
+ await t.aztecNodeAdmin.setConfig({ minTxsPerBlock: 1 });
}, 300_000);
+ it('reports YES for initialization status via public nullifier when instance is not registered', async () => {
+ const owner = defaultAccountAddress;
+ const { contract } = await StatefulTestContract.deploy(wallet, owner, 42).send({ from: defaultAccountAddress });
+
+ // StatefulTestContract has public functions with initialization checks, so during deployment and initialization
+ // it emits a public initialization nullifier. A wallet without the instance registered falls back to checking
+ // this nullifier.
+ const secondWallet = await TestWallet.create(aztecNode);
+ const metadata = await secondWallet.getContractMetadata(contract.address);
+ expect(metadata.instance).toBeUndefined();
+ expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.INITIALIZED);
+ });
+
describe('regressions', () => {
it('fails properly when trying to deploy a contract with a failing constructor with a pxe client with retries', async () => {
const { AZTEC_NODE_URL } = process.env;
diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts
index 5dbe52080cbd..ea5443226953 100644
--- a/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts
+++ b/yarn-project/end-to-end/src/e2e_deploy_contract/private_initialization.test.ts
@@ -4,6 +4,7 @@ import { publishContractClass, publishInstance } from '@aztec/aztec.js/deploymen
import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import type { AztecNode } from '@aztec/aztec.js/node';
+import { ContractInitializationStatus } from '@aztec/aztec.js/wallet';
import { InitTestContract } from '@aztec/noir-test-contracts.js/InitTest';
import { NoConstructorContract } from '@aztec/noir-test-contracts.js/NoConstructor';
import { PrivateInitTestContract } from '@aztec/noir-test-contracts.js/PrivateInitTest';
@@ -224,6 +225,32 @@ describe('e2e_deploy_contract private initialization', () => {
);
});
+ describe('initialization status', () => {
+ it('reports INITIALIZED when contract is registered and initialized', async () => {
+ const contract = await t.registerContract(wallet, PrivateInitTestContract, {
+ initArgs: [42],
+ constructorName: 'initialize',
+ });
+ await contract.methods.initialize(42).send({ from: defaultAccountAddress });
+ const metadata = await wallet.getContractMetadata(contract.address);
+ expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.INITIALIZED);
+ });
+
+ it('reports UNINITIALIZED when contract is registered but not initialized', async () => {
+ const contract = await t.registerContract(wallet, PrivateInitTestContract, {
+ initArgs: [42],
+ constructorName: 'initialize',
+ });
+ const metadata = await wallet.getContractMetadata(contract.address);
+ expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.UNINITIALIZED);
+ });
+
+ it('reports UNKNOWN when contract instance is not registered', async () => {
+ const metadata = await wallet.getContractMetadata(await AztecAddress.random());
+ expect(metadata.initializationStatus).toEqual(ContractInitializationStatus.UNKNOWN);
+ });
+ });
+
/** Registers a contract instance locally and publishes it on-chain (so sequencers can find public function's bytecode). */
async function registerAndPublishContract(
initArgs: InitTestCtorArgs,
diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts
index 4b876b9aa659..dc21fe15ac84 100644
--- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts
+++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts
@@ -9,19 +9,20 @@ import {
} from '@aztec/aztec.js/contracts';
import type { FeePaymentMethod } from '@aztec/aztec.js/fee';
import { waitForTx } from '@aztec/aztec.js/node';
-import type {
- Aliased,
- AppCapabilities,
- BatchResults,
- BatchedMethod,
- ExecuteUtilityOptions,
- PrivateEvent,
- PrivateEventFilter,
- ProfileOptions,
- SendOptions,
- SimulateOptions,
- Wallet,
- WalletCapabilities,
+import {
+ type Aliased,
+ type AppCapabilities,
+ type BatchResults,
+ type BatchedMethod,
+ ContractInitializationStatus,
+ type ExecuteUtilityOptions,
+ type PrivateEvent,
+ type PrivateEventFilter,
+ type ProfileOptions,
+ type SendOptions,
+ type SimulateOptions,
+ type Wallet,
+ type WalletCapabilities,
} from '@aztec/aztec.js/wallet';
import {
GAS_ESTIMATION_DA_GAS_LIMIT,
@@ -52,7 +53,10 @@ import {
} from '@aztec/stdlib/contract';
import { SimulationError } from '@aztec/stdlib/errors';
import { Gas, GasSettings } from '@aztec/stdlib/gas';
-import { computeSiloedPrivateInitializationNullifier } from '@aztec/stdlib/hash';
+import {
+ computeSiloedPrivateInitializationNullifier,
+ computeSiloedPublicInitializationNullifier,
+} from '@aztec/stdlib/hash';
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
import {
BlockHeader,
@@ -494,26 +498,29 @@ export abstract class BaseWallet implements Wallet {
/**
* Returns metadata about a contract, including whether it has been initialized, published, and updated.
- *
- * `isContractInitialized` requires the contract instance to be registered in the PXE (for `init_hash`). When the
- * instance is not available, `isContractInitialized` is `undefined` since it cannot be determined.
* @param address - The contract address to query.
*/
async getContractMetadata(address: AztecAddress) {
const instance = await this.pxe.getContractInstance(address);
const publiclyRegisteredContractPromise = this.aztecNode.getContract(address);
- // We check only the private initialization nullifier. It is emitted by both private and public initializers and
- // includes init_hash, preventing observers from determining initialization status from the address alone. Without
- // the instance (and thus init_hash), we can't compute it, so we return undefined.
- //
- // We skip the public initialization nullifier because it's not always emitted (contracts without public external
- // functions that require initialization checks won't emit it). If the private one exists, the public one was
- // created in the same tx and will also be present.
- let isContractInitialized: boolean | undefined = undefined;
+
+ let initializationStatus: ContractInitializationStatus;
if (instance) {
+ // We have the instance, so we can compute the private initialization nullifier (which includes init_hash and is
+ // emitted by both private and public initializers) and get a definitive INITIALIZED/UNINITIALIZED answer.
const initNullifier = await computeSiloedPrivateInitializationNullifier(address, instance.initializationHash);
const witness = await this.aztecNode.getNullifierMembershipWitness('latest', initNullifier);
- isContractInitialized = !!witness;
+ initializationStatus = witness
+ ? ContractInitializationStatus.INITIALIZED
+ : ContractInitializationStatus.UNINITIALIZED;
+ } else {
+ // Without the instance we lack the init_hash needed for the private nullifier. We fall back to checking the
+ // public initialization nullifier (computed from address alone). Not all contracts emit it (only those with
+ // public functions that require initialization checks), so its absence doesn't mean the contract is
+ // uninitialized.
+ const publicNullifier = await computeSiloedPublicInitializationNullifier(address);
+ const witness = await this.aztecNode.getNullifierMembershipWitness('latest', publicNullifier);
+ initializationStatus = witness ? ContractInitializationStatus.INITIALIZED : ContractInitializationStatus.UNKNOWN;
}
const publiclyRegisteredContract = await publiclyRegisteredContractPromise;
const isContractUpdated =
@@ -521,7 +528,7 @@ export abstract class BaseWallet implements Wallet {
!publiclyRegisteredContract.currentContractClassId.equals(publiclyRegisteredContract.originalContractClassId);
return {
instance: instance ?? undefined,
- isContractInitialized,
+ initializationStatus,
isContractPublished: !!publiclyRegisteredContract,
isContractUpdated: !!isContractUpdated,
updatedContractClassId: isContractUpdated ? publiclyRegisteredContract.currentContractClassId : undefined,
From a0a4f08f92f9eea5aed8002e7792df1a03001a3f Mon Sep 17 00:00:00 2001
From: Santiago Palladino
Date: Thu, 19 Mar 2026 18:57:48 -0300
Subject: [PATCH 02/10] fix(stdlib): zero-pad bufferFromFields when declared
length exceeds payload (#21802)
Ensures that `bufferFromFields` always returns a buffer with the length
requested in the first field of the array.
This protects against this method being called with a truncated array,
which could cause a wrong public bytecode commitment to be computed.
Note that this is currently not the case, since this function always
gets called with an array that's exactly
`CONTRACT_CLASS_LOG_SIZE_IN_FIELDS` long, which is greater than the
`MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS`.
Co-authored-by: Claude Opus 4.6 (1M context)
---
yarn-project/stdlib/src/abi/buffer.test.ts | 42 ++++++++++++++++++++++
yarn-project/stdlib/src/abi/buffer.ts | 29 ++++++++++++---
2 files changed, 67 insertions(+), 4 deletions(-)
diff --git a/yarn-project/stdlib/src/abi/buffer.test.ts b/yarn-project/stdlib/src/abi/buffer.test.ts
index 9c3329666c30..d4ca635d5b92 100644
--- a/yarn-project/stdlib/src/abi/buffer.test.ts
+++ b/yarn-project/stdlib/src/abi/buffer.test.ts
@@ -1,3 +1,5 @@
+import { Fr } from '@aztec/foundation/curves/bn254';
+
import { bufferAsFields, bufferFromFields } from './buffer.js';
describe('buffer', () => {
@@ -11,4 +13,44 @@ describe('buffer', () => {
const buffer = Buffer.from('1234567890abcdef'.repeat(10), 'hex');
expect(() => bufferAsFields(buffer, 3)).toThrow(/exceeds maximum size/);
});
+
+ it('pads with zeros when declared length exceeds payload', () => {
+ // Create a small buffer, encode it, then truncate the field array before decoding.
+ const buffer = Buffer.from('aabbccdd', 'hex'); // 4 bytes
+ const fields = bufferAsFields(buffer, 10);
+ // Declared length is 4 bytes, stored in fields[0]. Payload fields follow.
+ // Artificially inflate the declared length to 62 bytes (2 full fields).
+
+ const inflatedFields = [new Fr(62), ...fields.slice(1)];
+ const result = bufferFromFields(inflatedFields);
+ // Result should be exactly 62 bytes: original 4 bytes followed by 58 zero bytes.
+ expect(result.length).toBe(62);
+ expect(result.subarray(0, 4).toString('hex')).toEqual('aabbccdd');
+ expect(result.subarray(4).every(b => b === 0)).toBe(true);
+ });
+
+ it('pads with zeros when payload fields are truncated', () => {
+ // Simulate the blob reconstruction scenario: declared length says 93 bytes (3 fields),
+ // but only 1 payload field is present.
+
+ const payloadField = Fr.fromBuffer(
+ Buffer.from('00' + 'ab'.repeat(31), 'hex'), // 31 bytes of 0xab
+ );
+ // Declared length = 93 bytes (would need 3 fields), but only 1 field in payload.
+ const fields = [new Fr(93), payloadField];
+ const result = bufferFromFields(fields);
+ expect(result.length).toBe(93);
+ // First 31 bytes come from the single payload field.
+ expect(result.subarray(0, 31).every(b => b === 0xab)).toBe(true);
+ // Remaining 62 bytes are zero-padded.
+ expect(result.subarray(31).every(b => b === 0)).toBe(true);
+ });
+
+ it('returns exact buffer when payload matches declared length', () => {
+ const buffer = Buffer.from('ff'.repeat(31), 'hex'); // exactly 1 field of payload
+ const fields = bufferAsFields(buffer, 5);
+ const result = bufferFromFields(fields);
+ expect(result.length).toBe(31);
+ expect(result.toString('hex')).toEqual(buffer.toString('hex'));
+ });
});
diff --git a/yarn-project/stdlib/src/abi/buffer.ts b/yarn-project/stdlib/src/abi/buffer.ts
index 075143244254..42d4ea8eba8a 100644
--- a/yarn-project/stdlib/src/abi/buffer.ts
+++ b/yarn-project/stdlib/src/abi/buffer.ts
@@ -26,11 +26,32 @@ export function bufferAsFields(input: Buffer, targetLength: number): Fr[] {
}
/**
- * Recovers a buffer from an array of fields.
- * @param fields - An output from bufferAsFields.
- * @returns The recovered buffer.
+ * Recovers a buffer from an array of fields previously encoded with bufferAsFields.
+ *
+ * The first field encodes the byte length of the original buffer. The remaining fields
+ * each carry 31 bytes of payload (the leading byte of each 32-byte field element is skipped).
+ *
+ * If the declared byte length exceeds the bytes available from the payload fields, the result
+ * is zero-padded to the full declared length. This is important for correctness when the field
+ * array has been truncated (e.g. contract class logs reconstructed from blobs using a short
+ * emittedLength): without padding, the resulting buffer would be shorter than declared, causing
+ * bytecode commitment computations to diverge from what the circuit produced.
+ *
+ * @param fields - An output from bufferAsFields: [byteLength, ...payloadFields].
+ * @returns A buffer of exactly `byteLength` bytes.
*/
export function bufferFromFields(fields: Fr[]): Buffer {
const [length, ...payload] = fields;
- return Buffer.concat(payload.map(f => f.toBuffer().subarray(1))).subarray(0, length.toNumber());
+ const byteLength = length.toNumber();
+ const raw = Buffer.concat(payload.map(f => f.toBuffer().subarray(1)));
+ if (raw.length >= byteLength) {
+ return raw.subarray(0, byteLength);
+ }
+ // Pad with zeros if the declared length exceeds the available payload bytes.
+ // This ensures the returned buffer always matches the declared length, so that
+ // downstream bytecode commitment computations are consistent even when the
+ // source field array was truncated (e.g. reconstructed from blob with a short emittedLength).
+ const result = Buffer.alloc(byteLength);
+ raw.copy(result);
+ return result;
}
From 10828afa303af4b637761320647261e9d9a637a5 Mon Sep 17 00:00:00 2001
From: Santiago Palladino
Date: Thu, 19 Mar 2026 19:26:57 -0300
Subject: [PATCH 03/10] test(protocol-contracts): verify max-size bytecode fits
in contract class log (#21818)
Verify that `CONTRACT_CLASS_LOG_SIZE_IN_FIELDS` is large enough to hold
a max-size public bytecode alongside the `ContractClassPublishedEvent`
header fields. If these constants drift, contract class registration
could silently break.
Co-authored-by: Claude Opus 4.6 (1M context)
---
.../contract_class_published_event.test.ts | 49 ++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.test.ts b/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.test.ts
index 860533d58fe0..8647fc333f26 100644
--- a/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.test.ts
+++ b/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.test.ts
@@ -1,11 +1,20 @@
+import {
+ CONTRACT_CLASS_LOG_SIZE_IN_FIELDS,
+ CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE,
+ MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS,
+} from '@aztec/constants';
+import { Fr } from '@aztec/foundation/curves/bn254';
import { setupCustomSnapshotSerializers } from '@aztec/foundation/testing';
-import { ContractClassLog } from '@aztec/stdlib/logs';
+import { bufferAsFields } from '@aztec/stdlib/abi';
+import { ContractClassLog, ContractClassLogFields } from '@aztec/stdlib/logs';
+import { ProtocolContractAddress } from '../protocol_contract_data.js';
import { getSampleContractClassPublishedEventPayload } from '../tests/fixtures.js';
import { ContractClassPublishedEvent } from './contract_class_published_event.js';
describe('ContractClassPublishedEvent', () => {
beforeAll(() => setupCustomSnapshotSerializers(expect));
+
it('parses an event as emitted by the ContractClassRegistry', () => {
const log = ContractClassLog.fromBuffer(getSampleContractClassPublishedEventPayload());
expect(ContractClassPublishedEvent.isContractClassPublishedEvent(log)).toBe(true);
@@ -15,4 +24,42 @@ describe('ContractClassPublishedEvent', () => {
// See ./__snapshots__/README.md for how to update the snapshot.
expect(event).toMatchSnapshot();
});
+
+ it('fits a max-size public bytecode within CONTRACT_CLASS_LOG_SIZE_IN_FIELDS', () => {
+ // Create a bytecode that fills the maximum allowed size.
+ // bufferAsFields encodes as [length_in_bytes, ...31-byte-chunks, ...zero-padding] up to the target length.
+ // The max bytecode in bytes is (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * 31,
+ // since one field is used for the length prefix.
+ const maxBytecodeBytes = (MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS - 1) * 31;
+ const maxBytecode = Buffer.alloc(maxBytecodeBytes, 0xab);
+
+ // Encode the bytecode as fields (same encoding used in the Noir contract).
+ const bytecodeFields = bufferAsFields(maxBytecode, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS);
+ expect(bytecodeFields).toHaveLength(MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS);
+
+ // The event header: [magic, contractClassId, version, artifactHash, privateFunctionsRoot]
+ const headerFields = [
+ new Fr(CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE),
+ Fr.random(), // contractClassId
+ new Fr(1), // version
+ Fr.random(), // artifactHash
+ Fr.random(), // privateFunctionsRoot
+ ];
+
+ // This is the main assertion: the CONTRACT_CLASS_LOG_SIZE_IN_FIELDS is enough such that
+ // a max-size bytecode can be included in the event log together with the header fields
+ const totalFields = headerFields.length + bytecodeFields.length;
+ expect(totalFields).toBeLessThanOrEqual(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS);
+
+ // Verify it round-trips through ContractClassLog and ContractClassPublishedEvent.
+ const allFields = [...headerFields, ...bytecodeFields];
+ const padded = [...allFields, ...Array(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS - allFields.length).fill(Fr.ZERO)];
+ const logFields = new ContractClassLogFields(padded);
+ const log = new ContractClassLog(ProtocolContractAddress.ContractClassRegistry, logFields, allFields.length);
+
+ expect(ContractClassPublishedEvent.isContractClassPublishedEvent(log)).toBe(true);
+
+ const event = ContractClassPublishedEvent.fromLog(log);
+ expect(event.packedPublicBytecode).toEqual(maxBytecode);
+ });
});
From c9513dd65e080b7259fbed14225de169285396cc Mon Sep 17 00:00:00 2001
From: Aztec Bot <49558828+AztecBot@users.noreply.github.com>
Date: Thu, 19 Mar 2026 22:56:31 -0400
Subject: [PATCH 04/10] chore: port P2P mesh topic deflake fix to v4-next
(#21825)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Ports the P2P mesh connectivity fix from `next` to `v4-next` for the
`duplicate_attestation_slash` and `duplicate_proposal_slash` e2e tests.
Cherry-picked commit: `8680abcca7` — chore: deflake duplicate proposals
and attestations (#20990)
## Root Cause
`waitForP2PMeshConnectivity` only waited for the `tx` GossipSub topic
mesh to form. The slash tests also need `block_proposal` and
`checkpoint_proposal` meshes ready before sequencers start proposing,
otherwise proposals get dropped and offenses are never detected.
## Fix
- Added `topics` parameter to `waitForP2PMeshConnectivity` (defaults to
`[TopicType.tx]` for backward compat)
- Slash tests now wait for all 3 relevant topics before proceeding
- Also added `advanceToEpochBeforeProposer` helper so sequencers start
before the target epoch arrives
Co-authored-by: Michal Rzeszutko
---
.../duplicate_attestation_slash.test.ts | 37 ++++++++++++---
.../e2e_p2p/duplicate_proposal_slash.test.ts | 37 ++++++++++++---
.../end-to-end/src/e2e_p2p/p2p_network.ts | 39 ++++++++--------
yarn-project/end-to-end/src/e2e_p2p/shared.ts | 45 ++++++++++++++++++-
4 files changed, 127 insertions(+), 31 deletions(-)
diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts
index 2f68d908d458..3ebef6ac94da 100644
--- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts
+++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_attestation_slash.test.ts
@@ -4,6 +4,7 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { bufferToHex } from '@aztec/foundation/string';
import { OffenseType } from '@aztec/slasher';
+import { TopicType } from '@aztec/stdlib/p2p';
import { jest } from '@jest/globals';
import fs from 'fs';
@@ -15,7 +16,7 @@ import { shouldCollectMetrics } from '../fixtures/fixtures.js';
import { ATTESTER_PRIVATE_KEYS_START_INDEX, createNode } from '../fixtures/setup_p2p_test.js';
import { getPrivateKeyFromIndex } from '../fixtures/utils.js';
import { P2PNetworkTest } from './p2p_network.js';
-import { awaitCommitteeExists, awaitOffenseDetected } from './shared.js';
+import { awaitCommitteeExists, awaitEpochWithProposer, awaitOffenseDetected } from './shared.js';
const TEST_TIMEOUT = 600_000; // 10 minutes
@@ -141,6 +142,7 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
coinbase: coinbase1,
attestToEquivocatedProposals: true, // Attest to all proposals - creates duplicate attestations
broadcastEquivocatedProposals: true, // Don't abort checkpoint building on duplicate block proposals
+ dontStartSequencer: true,
},
t.ctx.dateProvider!,
BOOT_NODE_UDP_PORT + 1,
@@ -159,6 +161,7 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
coinbase: coinbase2,
attestToEquivocatedProposals: true, // Attest to all proposals - creates duplicate attestations
broadcastEquivocatedProposals: true, // Don't abort checkpoint building on duplicate block proposals
+ dontStartSequencer: true,
},
t.ctx.dateProvider!,
BOOT_NODE_UDP_PORT + 2,
@@ -172,7 +175,10 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
// Create honest nodes with unique validator keys (indices 1 and 2)
t.logger.warn('Creating honest nodes');
const honestNode1 = await createNode(
- t.ctx.aztecNodeConfig,
+ {
+ ...t.ctx.aztecNodeConfig,
+ dontStartSequencer: true,
+ },
t.ctx.dateProvider!,
BOOT_NODE_UDP_PORT + 3,
t.bootstrapNodeEnr,
@@ -182,7 +188,10 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
shouldCollectMetrics(),
);
const honestNode2 = await createNode(
- t.ctx.aztecNodeConfig,
+ {
+ ...t.ctx.aztecNodeConfig,
+ dontStartSequencer: true,
+ },
t.ctx.dateProvider!,
BOOT_NODE_UDP_PORT + 4,
t.bootstrapNodeEnr,
@@ -194,10 +203,27 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
nodes = [maliciousNode1, maliciousNode2, honestNode1, honestNode2];
- // Wait for P2P mesh and the committee to be fully formed before proceeding
- await t.waitForP2PMeshConnectivity(nodes, NUM_VALIDATORS);
+ // Wait for P2P mesh on all needed topics before starting sequencers
+ await t.waitForP2PMeshConnectivity(nodes, NUM_VALIDATORS, 30, 0.1, [
+ TopicType.tx,
+ TopicType.block_proposal,
+ TopicType.checkpoint_proposal,
+ ]);
await awaitCommitteeExists({ rollup, logger: t.logger });
+ // Advance to an epoch where the malicious proposer is selected
+ const epochCache = (honestNode1 as TestAztecNodeService).epochCache;
+ await awaitEpochWithProposer({
+ epochCache,
+ cheatCodes: t.ctx.cheatCodes.rollup,
+ targetProposer: maliciousProposerAddress,
+ logger: t.logger,
+ });
+
+ // Start all sequencers simultaneously
+ t.logger.warn('Starting all sequencers');
+ await Promise.all(nodes.map(n => n.getSequencer()!.start()));
+
// Wait for offenses to be detected
// We expect BOTH duplicate proposal AND duplicate attestation offenses
// The malicious proposer nodes create duplicate proposals (same key, different coinbase)
@@ -236,7 +262,6 @@ describe('e2e_p2p_duplicate_attestation_slash', () => {
}
// Verify that for each duplicate attestation offense, the attester for that slot is the malicious validator
- const epochCache = (honestNode1 as TestAztecNodeService).epochCache;
for (const offense of duplicateAttestationOffenses) {
const offenseSlot = SlotNumber(Number(offense.epochOrSlot));
const committeeInfo = await epochCache.getCommittee(offenseSlot);
diff --git a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts
index 374e4527d4ef..c0b6062acac6 100644
--- a/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts
+++ b/yarn-project/end-to-end/src/e2e_p2p/duplicate_proposal_slash.test.ts
@@ -4,6 +4,7 @@ import { EthAddress } from '@aztec/aztec.js/addresses';
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { bufferToHex } from '@aztec/foundation/string';
import { OffenseType } from '@aztec/slasher';
+import { TopicType } from '@aztec/stdlib/p2p';
import { jest } from '@jest/globals';
import fs from 'fs';
@@ -15,7 +16,7 @@ import { shouldCollectMetrics } from '../fixtures/fixtures.js';
import { ATTESTER_PRIVATE_KEYS_START_INDEX, createNode } from '../fixtures/setup_p2p_test.js';
import { getPrivateKeyFromIndex } from '../fixtures/utils.js';
import { P2PNetworkTest } from './p2p_network.js';
-import { awaitCommitteeExists, awaitOffenseDetected } from './shared.js';
+import { awaitCommitteeExists, awaitEpochWithProposer, awaitOffenseDetected } from './shared.js';
const TEST_TIMEOUT = 600_000; // 10 minutes
@@ -130,6 +131,7 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
validatorPrivateKey: maliciousPrivateKeyHex,
coinbase: coinbase1,
broadcastEquivocatedProposals: true,
+ dontStartSequencer: true,
},
t.ctx.dateProvider,
BOOT_NODE_UDP_PORT + 1,
@@ -147,6 +149,7 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
validatorPrivateKey: maliciousPrivateKeyHex,
coinbase: coinbase2,
broadcastEquivocatedProposals: true,
+ dontStartSequencer: true,
},
t.ctx.dateProvider,
BOOT_NODE_UDP_PORT + 2,
@@ -160,7 +163,10 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
// Create honest nodes with unique validator keys (indices 1 and 2)
t.logger.warn('Creating honest nodes');
const honestNode1 = await createNode(
- t.ctx.aztecNodeConfig,
+ {
+ ...t.ctx.aztecNodeConfig,
+ dontStartSequencer: true,
+ },
t.ctx.dateProvider,
BOOT_NODE_UDP_PORT + 3,
t.bootstrapNodeEnr,
@@ -170,7 +176,10 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
shouldCollectMetrics(),
);
const honestNode2 = await createNode(
- t.ctx.aztecNodeConfig,
+ {
+ ...t.ctx.aztecNodeConfig,
+ dontStartSequencer: true,
+ },
t.ctx.dateProvider,
BOOT_NODE_UDP_PORT + 4,
t.bootstrapNodeEnr,
@@ -182,10 +191,27 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
nodes = [maliciousNode1, maliciousNode2, honestNode1, honestNode2];
- // Wait for P2P mesh and the committee to be fully formed before proceeding
- await t.waitForP2PMeshConnectivity(nodes, NUM_VALIDATORS);
+ // Wait for P2P mesh on all needed topics before starting sequencers
+ await t.waitForP2PMeshConnectivity(nodes, NUM_VALIDATORS, 30, 0.1, [
+ TopicType.tx,
+ TopicType.block_proposal,
+ TopicType.checkpoint_proposal,
+ ]);
await awaitCommitteeExists({ rollup, logger: t.logger });
+ // Advance to an epoch where the malicious proposer is selected
+ const epochCache = (honestNode1 as TestAztecNodeService).epochCache;
+ await awaitEpochWithProposer({
+ epochCache,
+ cheatCodes: t.ctx.cheatCodes.rollup,
+ targetProposer: maliciousValidatorAddress,
+ logger: t.logger,
+ });
+
+ // Start all sequencers simultaneously
+ t.logger.warn('Starting all sequencers');
+ await Promise.all(nodes.map(n => n.getSequencer()!.start()));
+
// Wait for offense to be detected
// The honest nodes should detect the duplicate proposal from the malicious validator
t.logger.warn('Waiting for duplicate proposal offense to be detected...');
@@ -208,7 +234,6 @@ describe('e2e_p2p_duplicate_proposal_slash', () => {
}
// Verify that for each offense, the proposer for that slot is the malicious validator
- const epochCache = (honestNode1 as TestAztecNodeService).epochCache;
for (const offense of offenses) {
const offenseSlot = SlotNumber(Number(offense.epochOrSlot));
const proposerForSlot = await epochCache.getProposerAttesterAddressInSlot(offenseSlot);
diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
index 1f2120f28177..83e18d1bc4ea 100644
--- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
+++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts
@@ -408,6 +408,7 @@ export class P2PNetworkTest {
expectedNodeCount?: number,
timeoutSeconds = 30,
checkIntervalSeconds = 0.1,
+ topics: TopicType[] = [TopicType.tx],
) {
const nodeCount = expectedNodeCount ?? nodes.length;
const minPeerCount = nodeCount - 1;
@@ -434,26 +435,28 @@ export class P2PNetworkTest {
this.logger.warn('All nodes connected to P2P mesh');
- // Wait for GossipSub mesh to form for the tx topic.
+ // Wait for GossipSub mesh to form for all specified topics.
// We only require at least 1 mesh peer per node because GossipSub
// stops grafting once it reaches Dlo peers and won't fill the mesh to all available peers.
- this.logger.warn('Waiting for GossipSub mesh to form for tx topic...');
- await Promise.all(
- nodes.map(async (node, index) => {
- const p2p = node.getP2P();
- await retryUntil(
- async () => {
- const meshPeers = await p2p.getGossipMeshPeerCount(TopicType.tx);
- this.logger.debug(`Node ${index} has ${meshPeers} gossip mesh peers for tx topic`);
- return meshPeers >= 1 ? true : undefined;
- },
- `Node ${index} to have gossip mesh peers for tx topic`,
- timeoutSeconds,
- checkIntervalSeconds,
- );
- }),
- );
- this.logger.warn('All nodes have gossip mesh peers for tx topic');
+ for (const topic of topics) {
+ this.logger.warn(`Waiting for GossipSub mesh to form for ${topic} topic...`);
+ await Promise.all(
+ nodes.map(async (node, index) => {
+ const p2p = node.getP2P();
+ await retryUntil(
+ async () => {
+ const meshPeers = await p2p.getGossipMeshPeerCount(topic);
+ this.logger.debug(`Node ${index} has ${meshPeers} gossip mesh peers for ${topic} topic`);
+ return meshPeers >= 1 ? true : undefined;
+ },
+ `Node ${index} to have gossip mesh peers for ${topic} topic`,
+ timeoutSeconds,
+ checkIntervalSeconds,
+ );
+ }),
+ );
+ this.logger.warn(`All nodes have gossip mesh peers for ${topic} topic`);
+ }
}
async teardown() {
diff --git a/yarn-project/end-to-end/src/e2e_p2p/shared.ts b/yarn-project/end-to-end/src/e2e_p2p/shared.ts
index a74488fef1c0..656313537ec8 100644
--- a/yarn-project/end-to-end/src/e2e_p2p/shared.ts
+++ b/yarn-project/end-to-end/src/e2e_p2p/shared.ts
@@ -6,12 +6,13 @@ import { Fr } from '@aztec/aztec.js/fields';
import type { Logger } from '@aztec/aztec.js/log';
import { TxHash } from '@aztec/aztec.js/tx';
import type { RollupCheatCodes } from '@aztec/aztec/testing';
+import type { EpochCacheInterface } from '@aztec/epoch-cache';
import type {
EmpireSlashingProposerContract,
RollupContract,
TallySlashingProposerContract,
} from '@aztec/ethereum/contracts';
-import { EpochNumber } from '@aztec/foundation/branded-types';
+import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { timesAsync, unique } from '@aztec/foundation/collection';
import { EthAddress } from '@aztec/foundation/eth-address';
import { retryUntil } from '@aztec/foundation/retry';
@@ -150,6 +151,48 @@ export async function awaitCommitteeExists({
return committee!.map(c => c.toString() as `0x${string}`);
}
+/**
+ * Advance epochs until we find one where the target proposer is selected for at least one slot.
+ * With N validators and M slots per epoch, a specific proposer may not be selected in any given epoch.
+ * For example, with 4 validators and 2 slots/epoch, there is about a 44% chance per epoch.
+ */
+export async function awaitEpochWithProposer({
+ epochCache,
+ cheatCodes,
+ targetProposer,
+ logger,
+ maxAttempts = 20,
+}: {
+ epochCache: EpochCacheInterface;
+ cheatCodes: RollupCheatCodes;
+ targetProposer: EthAddress;
+ logger: Logger;
+ maxAttempts?: number;
+}): Promise {
+ const { epochDuration } = await cheatCodes.getConfig();
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const currentEpoch = await cheatCodes.getEpoch();
+ const startSlot = Number(currentEpoch) * Number(epochDuration);
+ const endSlot = startSlot + Number(epochDuration);
+
+ logger.info(`Checking epoch ${currentEpoch} (slots ${startSlot}-${endSlot - 1}) for proposer ${targetProposer}`);
+
+ for (let s = startSlot; s < endSlot; s++) {
+ const proposer = await epochCache.getProposerAttesterAddressInSlot(SlotNumber(s));
+ if (proposer && proposer.equals(targetProposer)) {
+ logger.warn(`Found target proposer ${targetProposer} in slot ${s} of epoch ${currentEpoch}`);
+ return;
+ }
+ }
+
+ logger.info(`Target proposer not found in epoch ${currentEpoch}, advancing to next epoch`);
+ await cheatCodes.advanceToNextEpoch();
+ }
+
+ throw new Error(`Target proposer ${targetProposer} not found in any slot after ${maxAttempts} epoch attempts`);
+}
+
export async function awaitOffenseDetected({
logger,
nodeAdmin,
From 139da7d9e7f494361cdf902f28ff13e4e157f106 Mon Sep 17 00:00:00 2001
From: PhilWindle <60546371+PhilWindle@users.noreply.github.com>
Date: Fri, 20 Mar 2026 09:24:49 +0000
Subject: [PATCH 05/10] fix(archiver): throw on duplicate contract class or
instance additions (#21799)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Motivation
The contract instance store used `.set()` which silently overwrote
existing entries, while the contract class store used
`.setIfNotExists()` which silently ignored duplicates. Neither behavior
catches the unexpected case of a double-add, which could lead to data
loss on rollback — if an instance is added at block N and again at block
M, rolling back block M would delete the instance entirely, invalidating
the first add. The protocol prevents this via deployer nullifiers, but
the store should enforce it as defense-in-depth.
## Approach
Both `addContractInstance` and `addContractClass` now check for existing
entries and throw if the key already exists. This surfaces any
unexpected double-adds as errors rather than silently corrupting state.
## Changes
- **archiver (contract_instance_store)**: `addContractInstance` checks
`hasAsync` before writing; throws with a descriptive error on duplicate
- **archiver (contract_class_store)**: `addContractClass` replaces
`setIfNotExists` with explicit `hasAsync` check and throw on duplicate
- **archiver (tests)**: Updated "add twice" tests to expect throws
instead of silent success
---
yarn-project/archiver/src/factory.ts | 8 +++-
.../src/store/contract_class_store.ts | 10 +++--
.../src/store/contract_instance_store.ts | 13 +++---
.../src/store/kv_archiver_store.test.ts | 41 +++++++------------
4 files changed, 36 insertions(+), 36 deletions(-)
diff --git a/yarn-project/archiver/src/factory.ts b/yarn-project/archiver/src/factory.ts
index bbf2f8d56948..f056d687296f 100644
--- a/yarn-project/archiver/src/factory.ts
+++ b/yarn-project/archiver/src/factory.ts
@@ -175,12 +175,18 @@ export async function createArchiver(
return archiver;
}
-/** Registers protocol contracts in the archiver store. */
+/** Registers protocol contracts in the archiver store. Idempotent — skips contracts that already exist (e.g. on node restart). */
export async function registerProtocolContracts(store: KVArchiverDataStore) {
const blockNumber = 0;
for (const name of protocolContractNames) {
const provider = new BundledProtocolContractsProvider();
const contract = await provider.getProtocolContractArtifact(name);
+
+ // Skip if already registered (happens on node restart with a persisted store).
+ if (await store.getContractClass(contract.contractClass.id)) {
+ continue;
+ }
+
const contractClassPublic: ContractClassPublic = {
...contract.contractClass,
privateFunctions: [],
diff --git a/yarn-project/archiver/src/store/contract_class_store.ts b/yarn-project/archiver/src/store/contract_class_store.ts
index 36de477aad42..ebe0d6f9400f 100644
--- a/yarn-project/archiver/src/store/contract_class_store.ts
+++ b/yarn-project/archiver/src/store/contract_class_store.ts
@@ -29,11 +29,15 @@ export class ContractClassStore {
blockNumber: number,
): Promise {
await this.db.transactionAsync(async () => {
- await this.#contractClasses.setIfNotExists(
- contractClass.id.toString(),
+ const key = contractClass.id.toString();
+ if (await this.#contractClasses.hasAsync(key)) {
+ throw new Error(`Contract class ${key} already exists, cannot add again at block ${blockNumber}`);
+ }
+ await this.#contractClasses.set(
+ key,
serializeContractClassPublic({ ...contractClass, l2BlockNumber: blockNumber }),
);
- await this.#bytecodeCommitments.setIfNotExists(contractClass.id.toString(), bytecodeCommitment.toBuffer());
+ await this.#bytecodeCommitments.set(key, bytecodeCommitment.toBuffer());
});
}
diff --git a/yarn-project/archiver/src/store/contract_instance_store.ts b/yarn-project/archiver/src/store/contract_instance_store.ts
index 63ea37ff9bcb..332605240e04 100644
--- a/yarn-project/archiver/src/store/contract_instance_store.ts
+++ b/yarn-project/archiver/src/store/contract_instance_store.ts
@@ -27,11 +27,14 @@ export class ContractInstanceStore {
addContractInstance(contractInstance: ContractInstanceWithAddress, blockNumber: number): Promise {
return this.db.transactionAsync(async () => {
- await this.#contractInstances.set(
- contractInstance.address.toString(),
- new SerializableContractInstance(contractInstance).toBuffer(),
- );
- await this.#contractInstancePublishedAt.set(contractInstance.address.toString(), blockNumber);
+ const key = contractInstance.address.toString();
+ if (await this.#contractInstances.hasAsync(key)) {
+ throw new Error(
+ `Contract instance at ${key} already exists (deployed at block ${await this.#contractInstancePublishedAt.getAsync(key)}), cannot add again at block ${blockNumber}`,
+ );
+ }
+ await this.#contractInstances.set(key, new SerializableContractInstance(contractInstance).toBuffer());
+ await this.#contractInstancePublishedAt.set(key, blockNumber);
});
}
diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts
index 3143da999025..5b7c188ccf78 100644
--- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts
+++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts
@@ -2201,14 +2201,14 @@ describe('KVArchiverDataStore', () => {
await expect(store.getContractClass(contractClass.id)).resolves.toBeUndefined();
});
- it('returns contract class if later "deployment" class was deleted', async () => {
- await store.addContractClasses(
- [contractClass],
- [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
- BlockNumber(blockNum + 1),
- );
- await store.deleteContractClasses([contractClass], BlockNumber(blockNum + 1));
- await expect(store.getContractClass(contractClass.id)).resolves.toMatchObject(contractClass);
+ it('throws if the same contract class is added again', async () => {
+ await expect(
+ store.addContractClasses(
+ [contractClass],
+ [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
+ BlockNumber(blockNum + 1),
+ ),
+ ).rejects.toThrow(/already exists/);
});
it('returns undefined if contract class is not found', async () => {
@@ -3089,22 +3089,17 @@ describe('KVArchiverDataStore', () => {
expect(storedBlock?.archive.root.equals(provisionalBlock.archive.root)).toBe(true);
});
- it('does not throw when adding the same contract class twice', async () => {
+ it('throws when adding the same contract class twice', async () => {
const contractClass = await makeContractClassPublic();
const commitment = await computePublicBytecodeCommitment(contractClass.packedBytecode);
- // Add contract class first time
await store.addContractClasses([contractClass], [commitment], BlockNumber(1));
-
- // Add same contract class again - should not throw (uses setIfNotExists)
- await store.addContractClasses([contractClass], [commitment], BlockNumber(2));
-
- // Verify contract class exists
- const retrieved = await store.getContractClass(contractClass.id);
- expect(retrieved).toBeDefined();
+ await expect(store.addContractClasses([contractClass], [commitment], BlockNumber(2))).rejects.toThrow(
+ /already exists/,
+ );
});
- it('does not throw when adding the same contract instance twice', async () => {
+ it('throws when adding the same contract instance twice', async () => {
const contractClass = await makeContractClassPublic();
await store.addContractClasses(
[contractClass],
@@ -3120,16 +3115,8 @@ describe('KVArchiverDataStore', () => {
address: await AztecAddress.random(),
};
- // Add contract instance first time
await store.addContractInstances([instance], BlockNumber(1));
-
- // Add same contract instance again - should not throw (uses set)
- await store.addContractInstances([instance], BlockNumber(2));
-
- // Verify instance exists
- const retrieved = await store.getContractInstance(instance.address, 1000n);
- expect(retrieved).toBeDefined();
- expect(retrieved?.address.equals(instance.address)).toBe(true);
+ await expect(store.addContractInstances([instance], BlockNumber(2))).rejects.toThrow(/already exists/);
});
it('does not duplicate logs when addLogs is called twice with same block', async () => {
From d9b251db18f1270788ab2f404ff2e6d95f263d9e Mon Sep 17 00:00:00 2001
From: Gregorio Juliana
Date: Fri, 20 Mar 2026 11:24:33 +0100
Subject: [PATCH 06/10] feat: sync poseidon in the browser (#21833)
https://github.com/AztecProtocol/aztec-packages/pull/20826 completely
tanks performance in the browser. PXE does a lot of hashing and the
`SharedArrayBuffer` comms overhead is way too much. This PR reverts to
the old behavior only in the browser.
---
.../src/crypto/poseidon/index.test.ts | 4 +-
.../foundation/src/crypto/poseidon/index.ts | 76 ++++++++++---------
2 files changed, 44 insertions(+), 36 deletions(-)
diff --git a/yarn-project/foundation/src/crypto/poseidon/index.test.ts b/yarn-project/foundation/src/crypto/poseidon/index.test.ts
index 84b6f39d6e06..abf4af662133 100644
--- a/yarn-project/foundation/src/crypto/poseidon/index.test.ts
+++ b/yarn-project/foundation/src/crypto/poseidon/index.test.ts
@@ -1,11 +1,11 @@
-import { Barretenberg } from '@aztec/bb.js';
+import { BarretenbergSync } from '@aztec/bb.js';
import { Fr } from '../../curves/bn254/field.js';
import { poseidon2Permutation } from './index.js';
describe('poseidon2Permutation', () => {
beforeAll(async () => {
- await Barretenberg.initSingleton({ threads: 1 });
+ await BarretenbergSync.initSingleton();
});
it('test vectors from cpp should match', async () => {
diff --git a/yarn-project/foundation/src/crypto/poseidon/index.ts b/yarn-project/foundation/src/crypto/poseidon/index.ts
index 601a1a000b65..adbd7a4b4eb4 100644
--- a/yarn-project/foundation/src/crypto/poseidon/index.ts
+++ b/yarn-project/foundation/src/crypto/poseidon/index.ts
@@ -1,21 +1,35 @@
-import { Barretenberg } from '@aztec/bb.js';
+import { Barretenberg, BarretenbergSync } from '@aztec/bb.js';
import { Fr } from '../../curves/bn254/field.js';
import { type Fieldable, serializeToFields } from '../../serialize/serialize.js';
+const IS_BROWSER = typeof window !== 'undefined';
+
+async function poseidon2HashFields(inputFields: Fr[]): Promise {
+ if (IS_BROWSER) {
+ await BarretenbergSync.initSingleton();
+ const api = BarretenbergSync.getSingleton();
+ const response = api.poseidon2Hash({
+ inputs: inputFields.map(i => i.toBuffer()),
+ });
+ return Fr.fromBuffer(Buffer.from(response.hash));
+ } else {
+ await Barretenberg.initSingleton();
+ const api = Barretenberg.getSingleton();
+ const response = await api.poseidon2Hash({
+ inputs: inputFields.map(i => i.toBuffer()),
+ });
+ return Fr.fromBuffer(Buffer.from(response.hash));
+ }
+}
+
/**
* Create a poseidon hash (field) from an array of input fields.
* @param input - The input fields to hash.
* @returns The poseidon hash.
*/
-export async function poseidon2Hash(input: Fieldable[]): Promise {
- const inputFields = serializeToFields(input);
- await Barretenberg.initSingleton();
- const api = Barretenberg.getSingleton();
- const response = await api.poseidon2Hash({
- inputs: inputFields.map(i => i.toBuffer()),
- });
- return Fr.fromBuffer(Buffer.from(response.hash));
+export function poseidon2Hash(input: Fieldable[]): Promise {
+ return poseidon2HashFields(serializeToFields(input));
}
/**
@@ -24,15 +38,10 @@ export async function poseidon2Hash(input: Fieldable[]): Promise {
* @param separator - The domain separator.
* @returns The poseidon hash.
*/
-export async function poseidon2HashWithSeparator(input: Fieldable[], separator: number): Promise {
+export function poseidon2HashWithSeparator(input: Fieldable[], separator: number): Promise {
const inputFields = serializeToFields(input);
inputFields.unshift(new Fr(separator));
- await Barretenberg.initSingleton();
- const api = Barretenberg.getSingleton();
- const response = await api.poseidon2Hash({
- inputs: inputFields.map(i => i.toBuffer()),
- });
- return Fr.fromBuffer(Buffer.from(response.hash));
+ return poseidon2HashFields(inputFields);
}
/**
@@ -42,19 +51,24 @@ export async function poseidon2HashWithSeparator(input: Fieldable[], separator:
*/
export async function poseidon2Permutation(input: Fieldable[]): Promise {
const inputFields = serializeToFields(input);
- // We'd like this assertion but it's not possible to use it in the browser.
- // assert(input.length === 4, 'Input state must be of size 4');
- await Barretenberg.initSingleton();
- const api = Barretenberg.getSingleton();
- const response = await api.poseidon2Permutation({
- inputs: inputFields.map(i => i.toBuffer()),
- });
- // We'd like this assertion but it's not possible to use it in the browser.
- // assert(response.outputs.length === 4, 'Output state must be of size 4');
- return response.outputs.map(o => Fr.fromBuffer(Buffer.from(o)));
+ if (IS_BROWSER) {
+ await BarretenbergSync.initSingleton();
+ const api = BarretenbergSync.getSingleton();
+ const response = api.poseidon2Permutation({
+ inputs: inputFields.map(i => i.toBuffer()),
+ });
+ return response.outputs.map(o => Fr.fromBuffer(Buffer.from(o)));
+ } else {
+ await Barretenberg.initSingleton();
+ const api = Barretenberg.getSingleton();
+ const response = await api.poseidon2Permutation({
+ inputs: inputFields.map(i => i.toBuffer()),
+ });
+ return response.outputs.map(o => Fr.fromBuffer(Buffer.from(o)));
+ }
}
-export async function poseidon2HashBytes(input: Buffer): Promise {
+export function poseidon2HashBytes(input: Buffer): Promise {
const inputFields = [];
for (let i = 0; i < input.length; i += 31) {
const fieldBytes = Buffer.alloc(32, 0);
@@ -65,11 +79,5 @@ export async function poseidon2HashBytes(input: Buffer): Promise {
inputFields.push(Fr.fromBuffer(fieldBytes));
}
- await Barretenberg.initSingleton();
- const api = Barretenberg.getSingleton();
- const response = await api.poseidon2Hash({
- inputs: inputFields.map(i => i.toBuffer()),
- });
-
- return Fr.fromBuffer(Buffer.from(response.hash));
+ return poseidon2HashFields(inputFields);
}
From c0b21acc48c2261a1fb57940e06a3017294e034d Mon Sep 17 00:00:00 2001
From: Aztec Bot <49558828+AztecBot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:54:03 -0400
Subject: [PATCH 07/10] chore: backport #21824 (fix(aztec-up): add sensible
defaults to installer y/n prompts) to v4-next (#21844)
---
aztec-up/bin/0.0.1/aztec-install | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/aztec-up/bin/0.0.1/aztec-install b/aztec-up/bin/0.0.1/aztec-install
index a217e7b134d4..b4074bb8fbef 100755
--- a/aztec-up/bin/0.0.1/aztec-install
+++ b/aztec-up/bin/0.0.1/aztec-install
@@ -58,10 +58,10 @@ function title {
echo -e " ${bold}${g}aztec-up${r} - a tool to install and manage aztec toolchain versions."
echo -e " ${bold}${g}aztec-wallet${r} - our minimalistic CLI wallet"
echo
- read -p "Do you wish to continue? (y/n) " -n 1 -r
+ read -p "Do you wish to continue? (Y/n) " -n 1 -r
echo
echo
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ if [[ ! $REPLY =~ ^[Yy]?$ ]]; then
exit 1
fi
}
@@ -83,7 +83,7 @@ function check_for_old_install {
echo_yellow "If you continue, the entire $AZTEC_HOME directory will be removed and replaced with the new installation."
echo "You should manually remove old docker images you no longer need."
echo
- read -p "Do you wish to continue? (y/n) " -n 1 -r
+ read -p "Do you wish to continue? (y/N) " -n 1 -r
echo
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
From 026e17b0e4434953b0d50f25d74dc0194d525dfe Mon Sep 17 00:00:00 2001
From: Aztec Bot <49558828+AztecBot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 11:25:26 -0400
Subject: [PATCH 08/10] fix(sequencer): backport wall-clock time for slot
estimation to v4-next (#21769) (#21847)
## Summary
Backport of https://github.com/AztecProtocol/aztec-packages/pull/21769
to v4-next.
When an Ethereum slot is missed (no block produced), the next L1 block
lands 24+ seconds after the previous one instead of 12. Computing the
next block timestamp as `getBlock().timestamp + ethereumSlotDuration`
produces wrong results, causing incorrect L2 slot, committee, and
proposer calculations.
This backport replaces all `getBlock().timestamp + ethereumSlotDuration`
patterns with a new `getNextL1SlotTimestamp()` helper that rounds
wall-clock time up to the next L1 slot boundary. Additionally,
`getSyncedL2SlotNumber` now uses the latest synced checkpoint slot as a
second signal to determine sync progress.
## Changes from original PR adapted for v4-next
- Cherry-pick had 4 conflicted files (epoch_cache, rollup,
sequencer-publisher, publisher test)
- Removed `getTargetEpochAndSlotInNextL1Slot` (pipelining not present on
v4-next)
- Removed publisher rotation tests (feature not present on v4-next)
- Used `dateProvider.nowInSeconds()` directly instead of removed
`EpochCache.nowInSeconds()` method
- Renamed `canProposeAtNextEthBlock` to `canProposeAt` on RollupContract
and SequencerPublisher
Fixes #14766
ClaudeBox log: https://claudebox.work/s/76e2a4f0177d755a?run=1
---
.../.claude/rules/typescript-style.md | 18 ++-
yarn-project/archiver/src/archiver.ts | 34 ++--
.../aztec-node/src/aztec-node/server.ts | 16 +-
.../e2e_epochs/epochs_missed_l1_slot.test.ts | 152 ++++++++++++++++++
.../e2e_l1_publisher/e2e_l1_publisher.test.ts | 12 +-
yarn-project/epoch-cache/src/epoch_cache.ts | 16 +-
yarn-project/ethereum/src/contracts/rollup.ts | 12 +-
.../foundation/src/branded-types/slot.ts | 5 +
.../src/client/sequencer-client.ts | 14 +-
.../global_variable_builder/global_builder.ts | 46 +++---
.../src/global_variable_builder/index.ts | 2 +-
.../src/publisher/sequencer-publisher.test.ts | 2 +
.../src/publisher/sequencer-publisher.ts | 30 +++-
.../src/sequencer/sequencer.test.ts | 12 +-
.../src/sequencer/sequencer.ts | 6 +-
.../stdlib/src/block/l2_block_source.ts | 6 +-
.../stdlib/src/epoch-helpers/index.ts | 11 ++
17 files changed, 304 insertions(+), 90 deletions(-)
create mode 100644 yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts
diff --git a/yarn-project/.claude/rules/typescript-style.md b/yarn-project/.claude/rules/typescript-style.md
index 5ea500e2e736..0990cc1f0c00 100644
--- a/yarn-project/.claude/rules/typescript-style.md
+++ b/yarn-project/.claude/rules/typescript-style.md
@@ -330,4 +330,20 @@ mock.getData.mockImplementation((id: string) => {
}
return Promise.resolve(undefined);
});
-```
\ No newline at end of file
+```
+
+## Arrow Function Bodies
+
+Use expression bodies instead of block bodies when the block only contains a `return`:
+
+```typescript
+// Good: Expression body
+items.map(item => item.value * 2)
+fn(arg => expression(arg, foo))
+
+// Bad: Block body with just a return
+items.map(item => { return item.value * 2; })
+fn(arg => { return expression(arg, foo); })
+```
+
+Block bodies are appropriate when the callback has multiple statements or side effects beyond the return.
\ No newline at end of file
diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts
index 3955cfc83c22..f77681a00824 100644
--- a/yarn-project/archiver/src/archiver.ts
+++ b/yarn-project/archiver/src/archiver.ts
@@ -342,19 +342,33 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
return Promise.resolve(this.synchronizer.getL1Timestamp());
}
- public getSyncedL2SlotNumber(): Promise {
+ public async getSyncedL2SlotNumber(): Promise {
+ // The synced L2 slot is the latest slot for which we have all L1 data,
+ // either because we have seen all L1 blocks for that slot, or because
+ // we have seen the corresponding checkpoint.
+
+ let slotFromL1Sync: SlotNumber | undefined;
const l1Timestamp = this.synchronizer.getL1Timestamp();
- if (l1Timestamp === undefined) {
- return Promise.resolve(undefined);
+ if (l1Timestamp !== undefined) {
+ const nextL1BlockSlot = getSlotAtNextL1Block(l1Timestamp, this.l1Constants);
+ if (Number(nextL1BlockSlot) > 0) {
+ slotFromL1Sync = SlotNumber.add(nextL1BlockSlot, -1);
+ }
+ }
+
+ let slotFromCheckpoint: SlotNumber | undefined;
+ const latestCheckpointNumber = await this.store.getSynchedCheckpointNumber();
+ if (latestCheckpointNumber > 0) {
+ const checkpointData = await this.store.getCheckpointData(latestCheckpointNumber);
+ if (checkpointData) {
+ slotFromCheckpoint = checkpointData.header.slotNumber;
+ }
}
- // The synced slot is the last L2 slot whose all L1 blocks have been processed.
- // If the next L1 block (at l1Timestamp + ethereumSlotDuration) falls in slot N,
- // then we've fully synced slot N-1.
- const nextL1BlockSlot = getSlotAtNextL1Block(l1Timestamp, this.l1Constants);
- if (Number(nextL1BlockSlot) === 0) {
- return Promise.resolve(undefined);
+
+ if (slotFromL1Sync === undefined && slotFromCheckpoint === undefined) {
+ return undefined;
}
- return Promise.resolve(SlotNumber(nextL1BlockSlot - 1));
+ return SlotNumber(Math.max(slotFromL1Sync ?? 0, slotFromCheckpoint ?? 0));
}
public async getSyncedL2EpochNumber(): Promise {
diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts
index 2a2355ff28f3..b2c3bdd99598 100644
--- a/yarn-project/aztec-node/src/aztec-node/server.ts
+++ b/yarn-project/aztec-node/src/aztec-node/server.ts
@@ -458,6 +458,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
})
.catch(err => log.error('Failed to start p2p services after archiver sync', err));
+ const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, {
+ l1Contracts: config.l1Contracts,
+ ethereumSlotDuration: config.ethereumSlotDuration,
+ rollupVersion: BigInt(config.rollupVersion),
+ l1GenesisTime,
+ slotDuration: Number(slotDuration),
+ });
+
// Validator enabled, create/start relevant service
let sequencer: SequencerClient | undefined;
let slasherClient: SlasherClientInterface | undefined;
@@ -520,6 +528,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
dateProvider,
blobClient,
nodeKeyStore: keyStoreManager!,
+ globalVariableBuilder,
});
}
@@ -553,13 +562,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
}
}
- const globalVariableBuilder = new GlobalVariableBuilder({
- ...config,
- rollupVersion: BigInt(config.rollupVersion),
- l1GenesisTime,
- slotDuration: Number(slotDuration),
- });
-
const node = new AztecNodeService(
config,
p2pClient,
diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts
new file mode 100644
index 000000000000..1fe3b1a9a47f
--- /dev/null
+++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_slot.test.ts
@@ -0,0 +1,152 @@
+import type { ChainMonitorEventMap } from '@aztec/ethereum/test';
+import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
+import { AbortError } from '@aztec/foundation/error';
+import { sleep } from '@aztec/foundation/sleep';
+import { executeTimeout } from '@aztec/foundation/timer';
+import { SequencerState } from '@aztec/sequencer-client';
+import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
+
+import { jest } from '@jest/globals';
+
+import { EpochsTestContext } from './epochs_test.js';
+
+jest.setTimeout(1000 * 60 * 10);
+
+// Validates that the sequencer can build a block in an L2 slot even when the archiver hasn't synced
+// all L1 blocks of the previous slot. This happens when an L1 slot is missed (no block produced).
+// The fix relies on getSyncedL2SlotNumber using the latest synced checkpoint slot as a signal,
+// bypassing the stale L1 timestamp when L1 blocks are missing.
+// Regression test for https://github.com/AztecProtocol/aztec-packages/issues/14766
+describe('e2e_epochs/epochs_missed_l1_slot', () => {
+ let test: EpochsTestContext;
+
+ // Use enough L1 slots per L2 slot to have room for pausing mining mid-slot.
+ // With 6 L1 slots per L2 slot (L1=8s, L2=48s), we have plenty of time to
+ // publish a checkpoint and pause mining without accidentally skipping a slot.
+ const L1_SLOTS_PER_L2_SLOT = 6;
+
+ beforeEach(async () => {
+ test = await EpochsTestContext.setup({
+ numberOfAccounts: 0,
+ minTxsPerBlock: 0,
+ aztecSlotDurationInL1Slots: L1_SLOTS_PER_L2_SLOT,
+ startProverNode: false,
+ aztecProofSubmissionEpochs: 1024,
+ disableAnvilTestWatcher: true,
+ enforceTimeTable: true,
+ });
+ });
+
+ afterEach(async () => {
+ jest.restoreAllMocks();
+ await test.teardown();
+ });
+
+ it('builds a block after missed L1 slots when previous checkpoint is synced', async () => {
+ const { logger, constants, monitor, context } = test;
+ const eth = context.cheatCodes.eth;
+ const L1_BLOCK_TIME = test.L1_BLOCK_TIME_IN_S;
+ const L2_SLOT_DURATION = test.L2_SLOT_DURATION_IN_S;
+
+ // Step 1: Wait for a checkpoint that's published NOT in the last L1 slot of its L2 slot.
+ // We need the checkpoint to land early enough that when we pause mining, the archiver's
+ // L1 timestamp is still in the middle of the slot (not at the end).
+ logger.info('Waiting for a checkpoint published early in its L2 slot...');
+ const checkpointEvent = await executeTimeout(
+ signal =>
+ new Promise((res, rej) => {
+ const handleCheckpoint = (...[ev]: ChainMonitorEventMap['checkpoint']) => {
+ // Skip the initial checkpoint (genesis state).
+ if (ev.checkpointNumber === 0) {
+ return;
+ }
+ const slotStart = getTimestampForSlot(ev.l2SlotNumber, constants);
+ const lastL1SlotStart = slotStart + BigInt(L2_SLOT_DURATION - L1_BLOCK_TIME);
+ if (ev.timestamp < lastL1SlotStart) {
+ logger.info(
+ `Checkpoint ${ev.checkpointNumber} in slot ${ev.l2SlotNumber} at L1 timestamp ${ev.timestamp}`,
+ { slotStart, lastL1SlotStart },
+ );
+ res(ev);
+ monitor.off('checkpoint', handleCheckpoint);
+ } else {
+ logger.info(
+ `Skipping checkpoint ${ev.checkpointNumber}: published at ${ev.timestamp} (last L1 slot starts at ${lastL1SlotStart})`,
+ );
+ }
+ };
+ signal.onabort = () => {
+ monitor.off('checkpoint', handleCheckpoint);
+ rej(new AbortError());
+ };
+ monitor.on('checkpoint', handleCheckpoint);
+ }),
+ 60_000,
+ 'Wait for early checkpoint',
+ );
+
+ const checkpointSlotNumber = checkpointEvent.l2SlotNumber;
+ const nextSlotNumber = SlotNumber(checkpointSlotNumber + 1);
+ const nextSlotTimestamp = Number(getTimestampForSlot(nextSlotNumber, constants));
+
+ logger.info(`Using checkpoint ${checkpointEvent.checkpointNumber} in L2 slot ${checkpointSlotNumber}`, {
+ nextSlotNumber,
+ nextSlotTimestamp,
+ });
+
+ // Step 2: Wait briefly for the sequencer to finish its current work cycle, then pause mining.
+ await sleep(1500);
+
+ logger.info('Pausing L1 block production (simulating missed L1 slots)...');
+ await eth.setAutomine(false);
+ await eth.setIntervalMining(0, { silent: true });
+
+ const frozenL1Timestamp = await eth.timestamp();
+ logger.info(`L1 mining paused at L1 timestamp ${frozenL1Timestamp}`);
+
+ // Step 3: Wait until the sequencer reaches PUBLISHING_CHECKPOINT during the mining pause.
+ // With the fix: the sequencer sees the checkpoint for slot N, so getSyncedL2SlotNumber
+ // returns N, checkSync passes for slot N+1, and it advances all the way to publishing.
+ // Without the fix: getSyncedL2SlotNumber is stuck at N-1, checkSync fails, sequencer
+ // stays in IDLE/SYNCHRONIZING and never reaches PUBLISHING_CHECKPOINT.
+ const sequencer = context.sequencer!.getSequencer();
+
+ logger.info('Waiting for sequencer to reach PUBLISHING_CHECKPOINT during mining pause...');
+ await executeTimeout(
+ signal =>
+ new Promise((res, rej) => {
+ const stateListener = ({ newState }: { newState: SequencerState }) => {
+ if (newState === SequencerState.PUBLISHING_CHECKPOINT) {
+ sequencer.off('state-changed', stateListener);
+ res();
+ }
+ };
+ signal.onabort = () => {
+ sequencer.off('state-changed', stateListener);
+ rej(new AbortError());
+ };
+ sequencer.on('state-changed', stateListener);
+ }),
+ L2_SLOT_DURATION * 2 * 1000,
+ 'Wait for sequencer to reach PUBLISHING_CHECKPOINT',
+ );
+
+ logger.info('Sequencer reached PUBLISHING_CHECKPOINT during mining pause');
+
+ // Step 4: Resume mining so the pending L1 tx lands and the test can clean up.
+ logger.info('Resuming L1 block production...');
+ const resumeTimestamp = Math.floor(context.dateProvider.now() / 1000);
+ await eth.setNextBlockTimestamp(resumeTimestamp);
+ await eth.mine();
+ await eth.setIntervalMining(L1_BLOCK_TIME);
+
+ // Step 5: Wait for the next checkpoint to confirm the block was actually published.
+ const finalCheckpoint = CheckpointNumber(checkpointEvent.checkpointNumber + 1);
+ logger.info(`Waiting for checkpoint ${finalCheckpoint}...`);
+ await test.waitUntilCheckpointNumber(finalCheckpoint, 60);
+ await monitor.run();
+ logger.info(`Checkpoint ${finalCheckpoint} published in slot ${monitor.l2SlotNumber}`);
+
+ expect(monitor.checkpointNumber).toBeGreaterThanOrEqual(finalCheckpoint);
+ });
+});
diff --git a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts
index 394f86adaa7e..2dafcdf0e69b 100644
--- a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts
+++ b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts
@@ -606,7 +606,7 @@ describe('L1Publisher integration', () => {
const checkpointAttestations = validators.map(v => makeCheckpointAttestationFromCheckpoint(checkpoint, v));
const attestations = orderAttestations(checkpointAttestations, committee!);
- const canPropose = await publisher.canProposeAtNextEthBlock(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
+ const canPropose = await publisher.canProposeAt(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
expect(canPropose?.slot).toEqual(block.header.getSlot());
await publisher.validateBlockHeader(checkpoint.header);
@@ -630,7 +630,7 @@ describe('L1Publisher integration', () => {
const attestations = orderAttestations(checkpointAttestations, committee!).reverse();
const attestationsAndSigners = new CommitteeAttestationsAndSigners(attestations);
- const canPropose = await publisher.canProposeAtNextEthBlock(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
+ const canPropose = await publisher.canProposeAt(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
expect(canPropose?.slot).toEqual(block.header.getSlot());
await publisher.validateBlockHeader(checkpoint.header);
@@ -645,7 +645,7 @@ describe('L1Publisher integration', () => {
const checkpointAttestations = validators.map(v => makeCheckpointAttestationFromCheckpoint(checkpoint, v));
const attestations = orderAttestations(checkpointAttestations, committee!);
- const canPropose = await publisher.canProposeAtNextEthBlock(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
+ const canPropose = await publisher.canProposeAt(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
expect(canPropose?.slot).toEqual(block.header.getSlot());
await publisher.validateBlockHeader(checkpoint.header);
@@ -670,7 +670,7 @@ describe('L1Publisher integration', () => {
const checkpointAttestations = validators.map(v => makeCheckpointAttestationFromCheckpoint(checkpoint, v));
const attestations = orderAttestations(checkpointAttestations, committee!);
- const canPropose = await publisher.canProposeAtNextEthBlock(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
+ const canPropose = await publisher.canProposeAt(new Fr(GENESIS_ARCHIVE_ROOT), proposer!);
expect(canPropose?.slot).toEqual(block.header.getSlot());
await publisher.validateBlockHeader(checkpoint.header);
@@ -742,8 +742,8 @@ describe('L1Publisher integration', () => {
// We cannot propose directly, we need to assume the previous checkpoint is invalidated
const genesis = new Fr(GENESIS_ARCHIVE_ROOT);
logger.warn(`Checking can propose at next eth block on top of genesis ${genesis}`);
- expect(await publisher.canProposeAtNextEthBlock(genesis, proposer!)).toBeUndefined();
- const canPropose = await publisher.canProposeAtNextEthBlock(genesis, proposer!, { forcePendingCheckpointNumber });
+ expect(await publisher.canProposeAt(genesis, proposer!)).toBeUndefined();
+ const canPropose = await publisher.canProposeAt(genesis, proposer!, { forcePendingCheckpointNumber });
expect(canPropose?.slot).toEqual(block.header.getSlot());
// Same for validation
diff --git a/yarn-project/epoch-cache/src/epoch_cache.ts b/yarn-project/epoch-cache/src/epoch_cache.ts
index e961706d815c..59271c34e3a6 100644
--- a/yarn-project/epoch-cache/src/epoch_cache.ts
+++ b/yarn-project/epoch-cache/src/epoch_cache.ts
@@ -9,6 +9,7 @@ import {
type L1RollupConstants,
getEpochAtSlot,
getEpochNumberAtTimestamp,
+ getNextL1SlotTimestamp,
getSlotAtTimestamp,
getSlotRangeForEpoch,
getTimestampForSlot,
@@ -148,10 +149,6 @@ export class EpochCache implements EpochCacheInterface {
return { ...this.getEpochAndSlotAtTimestamp(nowSeconds), nowMs };
}
- public nowInSeconds(): bigint {
- return BigInt(Math.floor(this.dateProvider.now() / 1000));
- }
-
private getEpochAndSlotAtSlot(slot: SlotNumber): EpochAndSlot {
const epoch = getEpochAtSlot(slot, this.l1constants);
const ts = getTimestampRangeForEpoch(epoch, this.l1constants)[0];
@@ -159,8 +156,8 @@ export class EpochCache implements EpochCacheInterface {
}
public getEpochAndSlotInNextL1Slot(): EpochAndSlot & { now: bigint } {
- const now = this.nowInSeconds();
- const nextSlotTs = now + BigInt(this.l1constants.ethereumSlotDuration);
+ const now = BigInt(this.dateProvider.nowInSeconds());
+ const nextSlotTs = getNextL1SlotTimestamp(Number(now), this.l1constants);
return { ...this.getEpochAndSlotAtTimestamp(nextSlotTs), now };
}
@@ -376,10 +373,11 @@ export class EpochCache implements EpochCacheInterface {
async getRegisteredValidators(): Promise {
const validatorRefreshIntervalMs = this.config.validatorRefreshIntervalSeconds * 1000;
const validatorRefreshTime = this.lastValidatorRefresh + validatorRefreshIntervalMs;
- if (validatorRefreshTime < this.dateProvider.now()) {
- const currentSet = await this.rollup.getAttesters();
+ const now = this.dateProvider.now();
+ if (validatorRefreshTime < now) {
+ const currentSet = await this.rollup.getAttesters(BigInt(Math.floor(now / 1000)));
this.allValidators = new Set(currentSet.map(v => v.toString()));
- this.lastValidatorRefresh = this.dateProvider.now();
+ this.lastValidatorRefresh = now;
}
return Array.from(this.allValidators.keys()).map(v => EthAddress.fromString(v));
}
diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts
index 980ff98ed12c..538b04d6aa82 100644
--- a/yarn-project/ethereum/src/contracts/rollup.ts
+++ b/yarn-project/ethereum/src/contracts/rollup.ts
@@ -777,14 +777,13 @@ export class RollupContract {
* timestamp of the next L1 block
* @throws otherwise
*/
- public async canProposeAtNextEthBlock(
+ public async canProposeAt(
archive: Buffer,
account: `0x${string}` | Account,
- slotDuration: number,
+ timestamp: bigint,
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
): Promise<{ slot: SlotNumber; checkpointNumber: CheckpointNumber; timeOfNextL1Slot: bigint }> {
- const latestBlock = await this.client.getBlock();
- const timeOfNextL1Slot = latestBlock.timestamp + BigInt(slotDuration);
+ const timeOfNextL1Slot = timestamp;
const who = typeof account === 'string' ? account : account.address;
try {
@@ -937,11 +936,10 @@ export class RollupContract {
return this.rollup.read.getSpecificProverRewardsForEpoch([epoch, prover]);
}
- async getAttesters(): Promise {
+ async getAttesters(timestamp?: bigint): Promise {
const attesterSize = await this.getActiveAttesterCount();
const gse = new GSEContract(this.client, await this.getGSE());
- const ts = (await this.client.getBlock()).timestamp;
-
+ const ts = timestamp ?? (await this.client.getBlock()).timestamp;
const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
const chunks = chunk(indices, 1000);
diff --git a/yarn-project/foundation/src/branded-types/slot.ts b/yarn-project/foundation/src/branded-types/slot.ts
index 069104a88fe0..2657cd622036 100644
--- a/yarn-project/foundation/src/branded-types/slot.ts
+++ b/yarn-project/foundation/src/branded-types/slot.ts
@@ -73,6 +73,11 @@ SlotNumber.isValid = function (value: unknown): value is SlotNumber {
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
};
+/** Increments a SlotNumber by a given value. */
+SlotNumber.add = function (sn: SlotNumber, increment: number): SlotNumber {
+ return SlotNumber(sn + increment);
+};
+
/**
* The zero slot value.
*/
diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts
index 6c00c41db92c..6afbb525df8a 100644
--- a/yarn-project/sequencer-client/src/client/sequencer-client.ts
+++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts
@@ -20,7 +20,7 @@ import { L1Metrics, type TelemetryClient } from '@aztec/telemetry-client';
import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
import { type SequencerClientConfig, getPublisherConfigFromSequencerConfig } from '../config.js';
-import { GlobalVariableBuilder } from '../global_variable_builder/index.js';
+import type { GlobalVariableBuilder } from '../global_variable_builder/index.js';
import { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
import { Sequencer, type SequencerConfig } from '../sequencer/index.js';
@@ -66,6 +66,7 @@ export class SequencerClient {
epochCache?: EpochCache;
l1TxUtils: L1TxUtils[];
nodeKeyStore: KeystoreManager;
+ globalVariableBuilder: GlobalVariableBuilder;
},
) {
const {
@@ -93,10 +94,9 @@ export class SequencerClient {
log.getBindings(),
);
const rollupContract = new RollupContract(publicClient, config.l1Contracts.rollupAddress.toString());
- const [l1GenesisTime, slotDuration, rollupVersion, rollupManaLimit] = await Promise.all([
+ const [l1GenesisTime, slotDuration, rollupManaLimit] = await Promise.all([
rollupContract.getL1GenesisTime(),
rollupContract.getSlotDuration(),
- rollupContract.getVersion(),
rollupContract.getManaLimit().then(Number),
] as const);
@@ -139,13 +139,7 @@ export class SequencerClient {
const ethereumSlotDuration = config.ethereumSlotDuration;
- const globalsBuilder = new GlobalVariableBuilder({
- ...config,
- l1GenesisTime,
- slotDuration: Number(slotDuration),
- ethereumSlotDuration,
- rollupVersion,
- });
+ const globalsBuilder = deps.globalVariableBuilder;
// When running in anvil, assume we can post a tx up until one second before the end of an L1 slot.
// Otherwise, we need the full L1 slot duration for publishing to ensure inclusion.
diff --git a/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts b/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts
index 20c0f236292d..3043f9a56eed 100644
--- a/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts
+++ b/yarn-project/sequencer-client/src/global_variable_builder/global_builder.ts
@@ -1,15 +1,13 @@
-import { createEthereumChain } from '@aztec/ethereum/chain';
-import { makeL1HttpTransport } from '@aztec/ethereum/client';
-import type { L1ContractsConfig } from '@aztec/ethereum/config';
import { RollupContract } from '@aztec/ethereum/contracts';
-import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
+import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
import type { ViemPublicClient } from '@aztec/ethereum/types';
import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { Fr } from '@aztec/foundation/curves/bn254';
import type { EthAddress } from '@aztec/foundation/eth-address';
import { createLogger } from '@aztec/foundation/log';
+import type { DateProvider } from '@aztec/foundation/timer';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
-import { type L1RollupConstants, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
+import { type L1RollupConstants, getNextL1SlotTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
import { GasFees } from '@aztec/stdlib/gas';
import type {
CheckpointGlobalVariables,
@@ -17,7 +15,12 @@ import type {
} from '@aztec/stdlib/tx';
import { GlobalVariables } from '@aztec/stdlib/tx';
-import { createPublicClient } from 'viem';
+/** Configuration for the GlobalVariableBuilder (excludes L1 client config). */
+export type GlobalVariableBuilderConfig = {
+ l1Contracts: Pick;
+ ethereumSlotDuration: number;
+ rollupVersion: bigint;
+} & Pick;
/**
* Simple global variables builder.
@@ -28,7 +31,6 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
private currentL1BlockNumber: bigint | undefined = undefined;
private readonly rollupContract: RollupContract;
- private readonly publicClient: ViemPublicClient;
private readonly ethereumSlotDuration: number;
private readonly aztecSlotDuration: number;
private readonly l1GenesisTime: bigint;
@@ -37,28 +39,18 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
private version: Fr;
constructor(
- config: L1ReaderConfig &
- Pick &
- Pick & { rollupVersion: bigint },
+ private readonly dateProvider: DateProvider,
+ private readonly publicClient: ViemPublicClient,
+ config: GlobalVariableBuilderConfig,
) {
- const { l1RpcUrls, l1ChainId: chainId, l1Contracts } = config;
-
- const chain = createEthereumChain(l1RpcUrls, chainId);
-
this.version = new Fr(config.rollupVersion);
- this.chainId = new Fr(chainId);
+ this.chainId = new Fr(this.publicClient.chain!.id);
this.ethereumSlotDuration = config.ethereumSlotDuration;
this.aztecSlotDuration = config.slotDuration;
this.l1GenesisTime = config.l1GenesisTime;
- this.publicClient = createPublicClient({
- chain: chain.chainInfo,
- transport: makeL1HttpTransport(chain.rpcUrls, { timeout: config.l1HttpTimeoutMS }),
- pollingInterval: config.viemPollingIntervalMS,
- });
-
- this.rollupContract = new RollupContract(this.publicClient, l1Contracts.rollupAddress);
+ this.rollupContract = new RollupContract(this.publicClient, config.l1Contracts.rollupAddress);
}
/**
@@ -74,7 +66,10 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
const earliestTimestamp = await this.rollupContract.getTimestampForSlot(
SlotNumber.fromBigInt(BigInt(lastCheckpoint.slotNumber) + 1n),
);
- const nextEthTimestamp = BigInt((await this.publicClient.getBlock()).timestamp + BigInt(this.ethereumSlotDuration));
+ const nextEthTimestamp = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), {
+ l1GenesisTime: this.l1GenesisTime,
+ ethereumSlotDuration: this.ethereumSlotDuration,
+ });
const timestamp = earliestTimestamp > nextEthTimestamp ? earliestTimestamp : nextEthTimestamp;
return new GasFees(0, await this.rollupContract.getManaMinFeeAt(timestamp, true));
@@ -109,7 +104,10 @@ export class GlobalVariableBuilder implements GlobalVariableBuilderInterface {
const slot: SlotNumber =
maybeSlot ??
(await this.rollupContract.getSlotAt(
- BigInt((await this.publicClient.getBlock()).timestamp + BigInt(this.ethereumSlotDuration)),
+ getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), {
+ l1GenesisTime: this.l1GenesisTime,
+ ethereumSlotDuration: this.ethereumSlotDuration,
+ }),
));
const checkpointGlobalVariables = await this.buildCheckpointGlobalVariables(coinbase, feeRecipient, slot);
diff --git a/yarn-project/sequencer-client/src/global_variable_builder/index.ts b/yarn-project/sequencer-client/src/global_variable_builder/index.ts
index 5669a0412ae4..a48ed6c244eb 100644
--- a/yarn-project/sequencer-client/src/global_variable_builder/index.ts
+++ b/yarn-project/sequencer-client/src/global_variable_builder/index.ts
@@ -1 +1 @@
-export { GlobalVariableBuilder } from './global_builder.js';
+export { GlobalVariableBuilder, type GlobalVariableBuilderConfig } from './global_builder.js';
diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts
index bf26a3ae16e1..12592d5a1042 100644
--- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts
+++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts
@@ -22,6 +22,7 @@ import { TestDateProvider } from '@aztec/foundation/timer';
import { EmpireBaseAbi, RollupAbi } from '@aztec/l1-artifacts';
import { CommitteeAttestationsAndSigners, L2Block, Signature } from '@aztec/stdlib/block';
import { Checkpoint } from '@aztec/stdlib/checkpoint';
+import { EmptyL1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import type { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
import { CheckpointHeader } from '@aztec/stdlib/rollup';
@@ -138,6 +139,7 @@ describe('SequencerPublisher', () => {
const epochCache = mock();
epochCache.getEpochAndSlotNow.mockReturnValue({ epoch: EpochNumber(1), slot: SlotNumber(2), ts: 3n, nowMs: 3000n });
+ epochCache.getL1Constants.mockReturnValue(EmptyL1RollupConstants);
epochCache.getCommittee.mockResolvedValue({
committee: [],
seed: 1n,
diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts
index ff1616869741..3ff0e0d87893 100644
--- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts
+++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts
@@ -41,6 +41,7 @@ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
+import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
@@ -121,6 +122,7 @@ export class SequencerPublisher {
protected log: Logger;
protected ethereumSlotDuration: bigint;
+ private dateProvider: DateProvider;
private blobClient: BlobClientInterface;
@@ -169,6 +171,7 @@ export class SequencerPublisher {
) {
this.log = deps.log ?? createLogger('sequencer:publisher');
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
+ this.dateProvider = deps.dateProvider;
this.epochCache = deps.epochCache;
this.lastActions = deps.lastActions;
@@ -450,11 +453,11 @@ export class SequencerPublisher {
}
/**
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
* @param tipArchive - The archive to check
* @returns The slot and block number if it is possible to propose, undefined otherwise
*/
- public canProposeAtNextEthBlock(
+ public async canProposeAt(
tipArchive: Fr,
msgSender: EthAddress,
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
@@ -462,8 +465,10 @@ export class SequencerPublisher {
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
+ const nextL1SlotTs = await this.getNextL1SlotTimestampWithL1Floor();
+
return this.rollupContract
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
})
.catch(err => {
@@ -500,7 +505,7 @@ export class SequencerPublisher {
flags,
] as const;
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
+ const ts = await this.getNextL1SlotTimestampWithL1Floor();
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
opts?.forcePendingCheckpointNumber,
);
@@ -1355,4 +1360,21 @@ export class SequencerPublisher {
},
});
}
+
+ /**
+ * Returns the timestamp to use when simulating L1 proposal calls.
+ * Uses the wall-clock-based next L1 slot boundary, but floors it with the latest L1 block timestamp
+ * plus one slot duration. This prevents the sequencer from targeting a future L2 slot when the L1
+ * chain hasn't caught up to the wall clock yet (e.g., the dateProvider is one L1 slot ahead of the
+ * latest mined block), which would cause the propose tx to land in an L1 block with block.timestamp
+ * still in the previous L2 slot.
+ * TODO(palla): Properly fix by keeping dateProvider synced with anvil's chain time on every block.
+ */
+ private async getNextL1SlotTimestampWithL1Floor(): Promise {
+ const l1Constants = this.epochCache.getL1Constants();
+ const fromWallClock = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
+ const latestBlock = await this.l1TxUtils.client.getBlock();
+ const fromL1Block = latestBlock.timestamp + BigInt(l1Constants.ethereumSlotDuration);
+ return fromWallClock > fromL1Block ? fromWallClock : fromL1Block;
+ }
}
diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
index c30a93fb770e..d6d6fcc80dce 100644
--- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
+++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
@@ -187,7 +187,7 @@ describe('sequencer', () => {
publisher.enqueueProposeCheckpoint.mockResolvedValue(undefined);
publisher.enqueueGovernanceCastSignal.mockResolvedValue(true);
publisher.enqueueSlashingActions.mockResolvedValue(true);
- publisher.canProposeAtNextEthBlock.mockResolvedValue({
+ publisher.canProposeAt.mockResolvedValue({
slot: SlotNumber(newSlotNumber),
checkpointNumber: CheckpointNumber.fromBlockNumber(newBlockNumber),
timeOfNextL1Slot: 1000n,
@@ -352,21 +352,21 @@ describe('sequencer', () => {
expect(checkpointBuilder.buildBlockCalls).toHaveLength(0);
expect(publisher.enqueueProposeCheckpoint).not.toHaveBeenCalled();
- expect(publisher.canProposeAtNextEthBlock).not.toHaveBeenCalled();
+ expect(publisher.canProposeAt).not.toHaveBeenCalled();
});
it('builds a checkpoint when it is their turn', async () => {
await setupSingleTxBlock();
- // Not your turn! canProposeAtNextEthBlock returns undefined
- publisher.canProposeAtNextEthBlock.mockResolvedValue(undefined);
+ // Not your turn! canProposeAt returns undefined
+ publisher.canProposeAt.mockResolvedValue(undefined);
await sequencer.work();
// When it's not our turn, we should not build the checkpoint
expect(checkpointBuilder.buildBlockCalls).toHaveLength(0);
// Now it's our turn!
- publisher.canProposeAtNextEthBlock.mockResolvedValue({
+ publisher.canProposeAt.mockResolvedValue({
slot: block.header.globalVariables.slotNumber,
checkpointNumber: CheckpointNumber.fromBlockNumber(block.header.globalVariables.blockNumber),
timeOfNextL1Slot: 1000n,
@@ -474,7 +474,7 @@ describe('sequencer', () => {
pub.enqueueProposeCheckpoint.mockResolvedValue(undefined);
pub.enqueueGovernanceCastSignal.mockResolvedValue(true);
pub.enqueueSlashingActions.mockResolvedValue(true);
- pub.canProposeAtNextEthBlock.mockResolvedValue({
+ pub.canProposeAt.mockResolvedValue({
slot: SlotNumber(newSlotNumber + i),
checkpointNumber: CheckpointNumber.fromBlockNumber(BlockNumber(newBlockNumber)),
timeOfNextL1Slot: 1000n,
diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts
index d75788ea3cf4..3af12aec8200 100644
--- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts
+++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts
@@ -327,7 +327,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter {
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
+ // The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
+ // See getSyncedL2SlotNumber for how missed L1 blocks are handled.
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
const { slot } = args;
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts
index 75f181a003f2..af020893af9c 100644
--- a/yarn-project/stdlib/src/block/l2_block_source.ts
+++ b/yarn-project/stdlib/src/block/l2_block_source.ts
@@ -176,8 +176,10 @@ export interface L2BlockSource {
getSettledTxReceipt(txHash: TxHash): Promise;
/**
- * Returns the last L2 slot number that has been fully synchronized from L1.
- * An L2 slot is fully synced when all L1 blocks that fall within its time range have been processed.
+ * Returns the last L2 slot number for which we have all L1 data needed to build the next checkpoint.
+ * Determined by the max of two signals: L1 block sync progress and latest synced checkpoint slot.
+ * The checkpoint signal handles missed L1 blocks, since a published checkpoint seals the message tree
+ * for the next checkpoint via the inbox LAG mechanism.
*/
getSyncedL2SlotNumber(): Promise;
diff --git a/yarn-project/stdlib/src/epoch-helpers/index.ts b/yarn-project/stdlib/src/epoch-helpers/index.ts
index 637afa3caf09..0ae35f5461f4 100644
--- a/yarn-project/stdlib/src/epoch-helpers/index.ts
+++ b/yarn-project/stdlib/src/epoch-helpers/index.ts
@@ -57,6 +57,17 @@ export function getSlotAtTimestamp(
: SlotNumber.fromBigInt((ts - constants.l1GenesisTime) / BigInt(constants.slotDuration));
}
+/** Returns the timestamp of the next L1 slot boundary after the given wall-clock time. */
+export function getNextL1SlotTimestamp(
+ nowInSeconds: number,
+ constants: Pick,
+): bigint {
+ const now = BigInt(nowInSeconds);
+ const elapsed = now - constants.l1GenesisTime;
+ const currentL1Slot = elapsed / BigInt(constants.ethereumSlotDuration);
+ return constants.l1GenesisTime + (currentL1Slot + 1n) * BigInt(constants.ethereumSlotDuration);
+}
+
/** Returns the L2 slot number at the next L1 block based on the current timestamp. */
export function getSlotAtNextL1Block(
currentL1Timestamp: bigint,
From 5402b0af20540af75eae0626c88054cf6a290ba3 Mon Sep 17 00:00:00 2001
From: Aztec Bot <49558828+AztecBot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 13:11:29 -0400
Subject: [PATCH 09/10] chore: backport PR #21788 (feat(p2p): add tx validation
for contract class id verification) to v4-next (#21852)
## Summary
Backport of https://github.com/AztecProtocol/aztec-packages/pull/21788
to v4-next.
Adds contract class ID validation to the `DataTxValidator` and
archiver's `updatePublishedContractClasses`, preventing malicious txs
from registering classes with mismatched IDs. Also simplifies
`toContractClassPublic()` to be synchronous with explicit validation at
call sites.
## Conflicts Resolved
Cherry-pick had 7 conflict regions across 5 files. Key differences on
v4-next:
- `ContractClassPublic` type includes
`privateFunctions`/`utilityFunctions` arrays (not present on next)
- Different import paths for `DataStoreConfig` and `L1RollupConstants`
- Different test expectations for duplicate contract class registration
Full conflict resolution analysis:
https://gist.github.com/AztecBot/7bae9da9cdc612e1df373324a58e6b24
## Test plan
- TypeScript compilation passes for all modified packages
- Existing tests should pass (test structure adapted for v4-next)
- CI will validate end-to-end"
ClaudeBox log: https://claudebox.work/s/4066b8ad44d62582?run=1
---------
Co-authored-by: Santiago Palladino
---
yarn-project/archiver/src/factory.ts | 9 +-
.../src/modules/data_store_updater.ts | 42 ++++--
.../src/store/kv_archiver_store.test.ts | 37 +++---
.../archiver/src/store/kv_archiver_store.ts | 12 +-
.../tx_validator/data_validator.test.ts | 123 +++++++++++++++++-
.../tx_validator/data_validator.ts | 43 +++++-
.../tx_validator/phases_validator.ts | 2 +-
.../contract_class_published_event.ts | 29 ++---
.../simulator/src/public/public_db_sources.ts | 24 ++--
.../public_processor/public_processor.test.ts | 4 +-
.../public_processor/public_processor.ts | 2 +-
.../contract_provider_for_cpp.ts | 3 +-
.../public_tx_simulator.ts | 4 +-
.../src/contract/contract_class_id.test.ts | 19 +++
.../stdlib/src/tx/validator/error_texts.ts | 8 ++
15 files changed, 279 insertions(+), 82 deletions(-)
diff --git a/yarn-project/archiver/src/factory.ts b/yarn-project/archiver/src/factory.ts
index f056d687296f..fd4f10b6549d 100644
--- a/yarn-project/archiver/src/factory.ts
+++ b/yarn-project/archiver/src/factory.ts
@@ -14,7 +14,7 @@ import { protocolContractNames } from '@aztec/protocol-contracts';
import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle';
import { FunctionType, decodeFunctionSignature } from '@aztec/stdlib/abi';
import type { ArchiverEmitter } from '@aztec/stdlib/block';
-import { type ContractClassPublic, computePublicBytecodeCommitment } from '@aztec/stdlib/contract';
+import { type ContractClassPublicWithCommitment, computePublicBytecodeCommitment } from '@aztec/stdlib/contract';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import { getTelemetryClient } from '@aztec/telemetry-client';
@@ -187,8 +187,10 @@ export async function registerProtocolContracts(store: KVArchiverDataStore) {
continue;
}
- const contractClassPublic: ContractClassPublic = {
+ const publicBytecodeCommitment = await computePublicBytecodeCommitment(contract.contractClass.packedBytecode);
+ const contractClassPublic: ContractClassPublicWithCommitment = {
...contract.contractClass,
+ publicBytecodeCommitment,
privateFunctions: [],
utilityFunctions: [],
};
@@ -198,8 +200,7 @@ export async function registerProtocolContracts(store: KVArchiverDataStore) {
.map(fn => decodeFunctionSignature(fn.name, fn.parameters));
await store.registerContractFunctionSignatures(publicFunctionSignatures);
- const bytecodeCommitment = await computePublicBytecodeCommitment(contractClassPublic.packedBytecode);
- await store.addContractClasses([contractClassPublic], [bytecodeCommitment], BlockNumber(blockNumber));
+ await store.addContractClasses([contractClassPublic], BlockNumber(blockNumber));
await store.addContractInstances([contract.instance], BlockNumber(blockNumber));
}
}
diff --git a/yarn-project/archiver/src/modules/data_store_updater.ts b/yarn-project/archiver/src/modules/data_store_updater.ts
index 6bf2975b3c3f..a490a80e84f9 100644
--- a/yarn-project/archiver/src/modules/data_store_updater.ts
+++ b/yarn-project/archiver/src/modules/data_store_updater.ts
@@ -13,9 +13,10 @@ import {
import type { L2Block, ValidateCheckpointResult } from '@aztec/stdlib/block';
import { type PublishedCheckpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
import {
+ type ContractClassPublicWithCommitment,
type ExecutablePrivateFunctionWithMembershipProof,
type UtilityFunctionWithMembershipProof,
- computePublicBytecodeCommitment,
+ computeContractClassId,
isValidPrivateFunctionMembershipProof,
isValidUtilityFunctionMembershipProof,
} from '@aztec/stdlib/contract';
@@ -321,18 +322,37 @@ export class ArchiverDataStoreUpdater {
.filter(log => ContractClassPublishedEvent.isContractClassPublishedEvent(log))
.map(log => ContractClassPublishedEvent.fromLog(log));
- const contractClasses = await Promise.all(contractClassPublishedEvents.map(e => e.toContractClassPublic()));
- if (contractClasses.length > 0) {
- contractClasses.forEach(c => this.log.verbose(`${Operation[operation]} contract class ${c.id.toString()}`));
- if (operation == Operation.Store) {
- // TODO: Will probably want to create some worker threads to compute these bytecode commitments as they are expensive
- const commitments = await Promise.all(
- contractClasses.map(c => computePublicBytecodeCommitment(c.packedBytecode)),
- );
- return await this.store.addContractClasses(contractClasses, commitments, blockNum);
- } else if (operation == Operation.Delete) {
+ if (operation == Operation.Delete) {
+ const contractClasses = contractClassPublishedEvents.map(e => e.toContractClassPublic());
+ if (contractClasses.length > 0) {
+ contractClasses.forEach(c => this.log.verbose(`${Operation[operation]} contract class ${c.id.toString()}`));
return await this.store.deleteContractClasses(contractClasses, blockNum);
}
+ return true;
+ }
+
+ // Compute bytecode commitments and validate class IDs in a single pass.
+ const contractClasses: ContractClassPublicWithCommitment[] = [];
+ for (const event of contractClassPublishedEvents) {
+ const contractClass = await event.toContractClassPublicWithBytecodeCommitment();
+ const computedClassId = await computeContractClassId({
+ artifactHash: contractClass.artifactHash,
+ privateFunctionsRoot: contractClass.privateFunctionsRoot,
+ publicBytecodeCommitment: contractClass.publicBytecodeCommitment,
+ });
+ if (!computedClassId.equals(contractClass.id)) {
+ this.log.warn(
+ `Skipping contract class with mismatched id at block ${blockNum}. Claimed ${contractClass.id}, computed ${computedClassId}`,
+ { blockNum, contractClassId: event.contractClassId.toString() },
+ );
+ continue;
+ }
+ contractClasses.push(contractClass);
+ }
+
+ if (contractClasses.length > 0) {
+ contractClasses.forEach(c => this.log.verbose(`${Operation[operation]} contract class ${c.id.toString()}`));
+ return await this.store.addContractClasses(contractClasses, blockNum);
}
return true;
}
diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts
index 5b7c188ccf78..ca9558c67c73 100644
--- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts
+++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts
@@ -25,6 +25,7 @@ import {
import { Checkpoint, PublishedCheckpoint, randomCheckpointInfo } from '@aztec/stdlib/checkpoint';
import {
type ContractClassPublic,
+ type ContractClassPublicWithCommitment,
type ContractInstanceWithAddress,
SerializableContractInstance,
computePublicBytecodeCommitment,
@@ -78,6 +79,13 @@ async function addProposedBlocks(
return result;
}
+async function withCommitment(contractClass: ContractClassPublic): Promise {
+ return {
+ ...contractClass,
+ publicBytecodeCommitment: await computePublicBytecodeCommitment(contractClass.packedBytecode),
+ };
+}
+
describe('KVArchiverDataStore', () => {
let store: KVArchiverDataStore;
let publishedCheckpoints: PublishedCheckpoint[];
@@ -2185,11 +2193,7 @@ describe('KVArchiverDataStore', () => {
beforeEach(async () => {
contractClass = await makeContractClassPublic();
- await store.addContractClasses(
- [contractClass],
- [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
- BlockNumber(blockNum),
- );
+ await store.addContractClasses([await withCommitment(contractClass)], BlockNumber(blockNum));
});
it('returns previously stored contract class', async () => {
@@ -2203,14 +2207,15 @@ describe('KVArchiverDataStore', () => {
it('throws if the same contract class is added again', async () => {
await expect(
- store.addContractClasses(
- [contractClass],
- [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
- BlockNumber(blockNum + 1),
- ),
+ store.addContractClasses([await withCommitment(contractClass)], BlockNumber(blockNum + 1)),
).rejects.toThrow(/already exists/);
});
+ it('returns contract class if deleted at a later block number', async () => {
+ await store.deleteContractClasses([contractClass], BlockNumber(blockNum + 1));
+ await expect(store.getContractClass(contractClass.id)).resolves.toMatchObject(contractClass);
+ });
+
it('returns undefined if contract class is not found', async () => {
await expect(store.getContractClass(Fr.random())).resolves.toBeUndefined();
});
@@ -3091,21 +3096,17 @@ describe('KVArchiverDataStore', () => {
it('throws when adding the same contract class twice', async () => {
const contractClass = await makeContractClassPublic();
- const commitment = await computePublicBytecodeCommitment(contractClass.packedBytecode);
+ const contractClassWithCommitment = await withCommitment(contractClass);
- await store.addContractClasses([contractClass], [commitment], BlockNumber(1));
- await expect(store.addContractClasses([contractClass], [commitment], BlockNumber(2))).rejects.toThrow(
+ await store.addContractClasses([contractClassWithCommitment], BlockNumber(1));
+ await expect(store.addContractClasses([contractClassWithCommitment], BlockNumber(2))).rejects.toThrow(
/already exists/,
);
});
it('throws when adding the same contract instance twice', async () => {
const contractClass = await makeContractClassPublic();
- await store.addContractClasses(
- [contractClass],
- [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
- BlockNumber(1),
- );
+ await store.addContractClasses([await withCommitment(contractClass)], BlockNumber(1));
const instance = {
...(await SerializableContractInstance.random({
diff --git a/yarn-project/archiver/src/store/kv_archiver_store.ts b/yarn-project/archiver/src/store/kv_archiver_store.ts
index 7db1342c198b..3279db17ef63 100644
--- a/yarn-project/archiver/src/store/kv_archiver_store.ts
+++ b/yarn-project/archiver/src/store/kv_archiver_store.ts
@@ -16,6 +16,7 @@ import {
import type { CheckpointData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
import type {
ContractClassPublic,
+ ContractClassPublicWithCommitment,
ContractDataSource,
ContractInstanceUpdateWithAddress,
ContractInstanceWithAddress,
@@ -168,19 +169,14 @@ export class KVArchiverDataStore implements ContractDataSource {
/**
* Add new contract classes from an L2 block to the store's list.
- * @param data - List of contract classes to be added.
- * @param bytecodeCommitments - Bytecode commitments for the contract classes.
+ * @param data - List of contract classes (with bytecode commitments) to be added.
* @param blockNumber - Number of the L2 block the contracts were registered in.
* @returns True if the operation is successful.
*/
- async addContractClasses(
- data: ContractClassPublic[],
- bytecodeCommitments: Fr[],
- blockNumber: BlockNumber,
- ): Promise {
+ async addContractClasses(data: ContractClassPublicWithCommitment[], blockNumber: BlockNumber): Promise {
return (
await Promise.all(
- data.map((c, i) => this.#contractClassStore.addContractClass(c, bytecodeCommitments[i], blockNumber)),
+ data.map(c => this.#contractClassStore.addContractClass(c, c.publicBytecodeCommitment, blockNumber)),
)
).every(Boolean);
}
diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts
index 2820c3a108b6..3f3c4ae17d00 100644
--- a/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts
+++ b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.test.ts
@@ -1,14 +1,18 @@
import {
CONTRACT_CLASS_LOG_SIZE_IN_FIELDS,
+ CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE,
MAX_CONTRACT_CLASS_LOGS_PER_TX,
MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS,
} from '@aztec/constants';
import { timesParallel } from '@aztec/foundation/collection';
import { randomInt } from '@aztec/foundation/crypto/random';
import { Fr } from '@aztec/foundation/curves/bn254';
+import { ProtocolContractAddress } from '@aztec/protocol-contracts';
+import { bufferAsFields } from '@aztec/stdlib/abi';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
+import { computeContractClassId, computePublicBytecodeCommitment } from '@aztec/stdlib/contract';
import { LogHash, ScopedLogHash } from '@aztec/stdlib/kernel';
-import { ContractClassLogFields } from '@aztec/stdlib/logs';
+import { ContractClassLog, ContractClassLogFields } from '@aztec/stdlib/logs';
import { mockTx } from '@aztec/stdlib/testing';
import {
TX_ERROR_CALLDATA_COUNT_MISMATCH,
@@ -17,6 +21,8 @@ import {
TX_ERROR_CONTRACT_CLASS_LOG_COUNT,
TX_ERROR_CONTRACT_CLASS_LOG_LENGTH,
TX_ERROR_INCORRECT_CALLDATA,
+ TX_ERROR_INCORRECT_CONTRACT_CLASS_ID,
+ TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG,
type Tx,
} from '@aztec/stdlib/tx';
@@ -243,4 +249,119 @@ describe('TxDataValidator', () => {
await expectInvalid(badTxs[0], TX_ERROR_CONTRACT_CLASS_LOG_LENGTH);
});
+
+ describe('contract class id validation', () => {
+ /**
+ * Builds a ContractClassLog encoding a ContractClassPublishedEvent.
+ * Layout: [magic, contractClassId, version, artifactHash, privateFunctionsRoot, ...bytecodeAsFields]
+ */
+ async function buildContractClassLog(opts?: { contractClassId?: Fr }): Promise<{
+ log: ContractClassLog;
+ emittedLength: number;
+ }> {
+ const artifactHash = Fr.random();
+ const privateFunctionsRoot = Fr.random();
+ const packedBytecode = Buffer.from('aabbccdd', 'hex');
+
+ const bytecodeCommitment = await computePublicBytecodeCommitment(packedBytecode);
+ const correctClassId = await computeContractClassId({
+ artifactHash,
+ privateFunctionsRoot,
+ publicBytecodeCommitment: bytecodeCommitment,
+ });
+ const contractClassId = opts?.contractClassId ?? correctClassId;
+
+ const bytecodeFields = bufferAsFields(packedBytecode, CONTRACT_CLASS_LOG_SIZE_IN_FIELDS);
+ let lastNonZero = bytecodeFields.length - 1;
+ while (lastNonZero >= 0 && bytecodeFields[lastNonZero].isZero()) {
+ lastNonZero--;
+ }
+ const bytecodeEmittedFields = bytecodeFields.slice(0, lastNonZero + 1);
+
+ const headerFields = [
+ new Fr(CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE),
+ contractClassId,
+ new Fr(1), // version
+ artifactHash,
+ privateFunctionsRoot,
+ ];
+
+ const emittedFields = [...headerFields, ...bytecodeEmittedFields];
+ const emittedLength = emittedFields.length;
+
+ const allFields = [
+ ...emittedFields,
+ ...Array(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS - emittedFields.length).fill(Fr.ZERO),
+ ];
+
+ const fields = new ContractClassLogFields(allFields);
+ const log = new ContractClassLog(ProtocolContractAddress.ContractClassRegistry, fields, emittedLength);
+ return { log, emittedLength };
+ }
+
+ async function injectContractClassLog(tx: Tx, log: ContractClassLog, emittedLength: number) {
+ tx.contractClassLogFields.push(log.fields);
+ const logHashes = tx.data.forPublic!.nonRevertibleAccumulatedData.contractClassLogsHashes;
+ const emptyIdx = logHashes.findIndex(h => h.isEmpty());
+ if (emptyIdx >= 0) {
+ logHashes[emptyIdx] = LogHash.from({
+ value: await log.fields.hash(),
+ length: emittedLength,
+ }).scope(log.contractAddress);
+ }
+ }
+
+ it('allows transactions with correct contract class ids', async () => {
+ const tx = await mockTx(2, {
+ numberOfNonRevertiblePublicCallRequests: 1,
+ numberOfRevertiblePublicCallRequests: 0,
+ });
+ const { log, emittedLength } = await buildContractClassLog();
+ await injectContractClassLog(tx, log, emittedLength);
+ await tx.recomputeHash();
+ await expect(validator.validateTx(tx)).resolves.toEqual({ result: 'valid' });
+ });
+
+ it('rejects transactions with incorrect contract class ids', async () => {
+ const tx = await mockTx(3, {
+ numberOfNonRevertiblePublicCallRequests: 1,
+ numberOfRevertiblePublicCallRequests: 0,
+ });
+ const { log, emittedLength } = await buildContractClassLog({ contractClassId: Fr.random() });
+ await injectContractClassLog(tx, log, emittedLength);
+ await tx.recomputeHash();
+ await expect(validator.validateTx(tx)).resolves.toEqual({
+ result: 'invalid',
+ reason: [TX_ERROR_INCORRECT_CONTRACT_CLASS_ID],
+ });
+ });
+
+ it('rejects transactions with malformed contract class logs', async () => {
+ const tx = await mockTx(4, {
+ numberOfNonRevertiblePublicCallRequests: 1,
+ numberOfRevertiblePublicCallRequests: 0,
+ });
+ const headerFields = [
+ new Fr(CONTRACT_CLASS_PUBLISHED_MAGIC_VALUE),
+ Fr.random(),
+ new Fr(1),
+ Fr.random(),
+ Fr.random(),
+ new Fr(999999), // bogus bytecode length
+ ];
+ const allFields = [
+ ...headerFields,
+ ...Array(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS - headerFields.length).fill(Fr.ZERO),
+ ];
+ const fields = new ContractClassLogFields(allFields);
+ const log = new ContractClassLog(ProtocolContractAddress.ContractClassRegistry, fields, headerFields.length);
+ await injectContractClassLog(tx, log, headerFields.length);
+ await tx.recomputeHash();
+ const result = await validator.validateTx(tx);
+ expect(result.result).toBe('invalid');
+ expect(result.result === 'invalid' && result.reason[0]).toMatch(
+ new RegExp(`${TX_ERROR_INCORRECT_CONTRACT_CLASS_ID}|${TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG}`),
+ );
+ });
+ });
});
diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.ts
index 692e4705e186..7c284b6d0ce3 100644
--- a/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.ts
+++ b/yarn-project/p2p/src/msg_validators/tx_validator/data_validator.ts
@@ -1,5 +1,7 @@
import { MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS } from '@aztec/constants';
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
+import { ContractClassPublishedEvent } from '@aztec/protocol-contracts/class-registry';
+import { computeContractClassId } from '@aztec/stdlib/contract';
import { computeCalldataHash } from '@aztec/stdlib/hash';
import {
TX_ERROR_CALLDATA_COUNT_MISMATCH,
@@ -9,7 +11,9 @@ import {
TX_ERROR_CONTRACT_CLASS_LOG_LENGTH,
TX_ERROR_CONTRACT_CLASS_LOG_SORTING,
TX_ERROR_INCORRECT_CALLDATA,
+ TX_ERROR_INCORRECT_CONTRACT_CLASS_ID,
TX_ERROR_INCORRECT_HASH,
+ TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG,
Tx,
type TxValidationResult,
type TxValidator,
@@ -26,7 +30,8 @@ export class DataTxValidator implements TxValidator {
const reason =
(await this.#hasCorrectHash(tx)) ??
(await this.#hasCorrectCalldata(tx)) ??
- (await this.#hasCorrectContractClassLogs(tx));
+ (await this.#hasCorrectContractClassLogs(tx)) ??
+ (await this.#hasCorrectContractClassIds(tx));
return reason ? { result: 'invalid', reason: [reason] } : { result: 'valid' };
}
@@ -127,4 +132,40 @@ export class DataTxValidator implements TxValidator {
return undefined;
}
+
+ async #hasCorrectContractClassIds(tx: Tx): Promise {
+ const contractClassLogs = tx.getContractClassLogs();
+ for (const log of contractClassLogs) {
+ if (!ContractClassPublishedEvent.isContractClassPublishedEvent(log)) {
+ continue;
+ }
+
+ let event;
+ try {
+ event = ContractClassPublishedEvent.fromLog(log);
+ } catch (e) {
+ this.#log.warn(`Rejecting tx ${tx.getTxHash()}: failed to parse contract class event: ${e}`);
+ return TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG;
+ }
+
+ try {
+ const { publicBytecodeCommitment } = await event.toContractClassPublicWithBytecodeCommitment();
+ const computedClassId = await computeContractClassId({
+ artifactHash: event.artifactHash,
+ privateFunctionsRoot: event.privateFunctionsRoot,
+ publicBytecodeCommitment,
+ });
+ if (!computedClassId.equals(event.contractClassId)) {
+ this.#log.warn(
+ `Rejecting tx ${tx.getTxHash()}: contract class id mismatch. Claimed ${event.contractClassId}, computed ${computedClassId}`,
+ );
+ return TX_ERROR_INCORRECT_CONTRACT_CLASS_ID;
+ }
+ } catch (e) {
+ this.#log.warn(`Rejecting tx ${tx.getTxHash()}: failed to compute contract class id: ${e}`);
+ return TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG;
+ }
+ }
+ return undefined;
+ }
}
diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts b/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts
index 69d5bd9f0cab..39362bd39dd2 100644
--- a/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts
+++ b/yarn-project/p2p/src/msg_validators/tx_validator/phases_validator.ts
@@ -40,7 +40,7 @@ export class PhasesTxValidator implements TxValidator {
// which are needed for public FPC flows, but fail if the account contract hasnt been deployed yet,
// which is what we're trying to do as part of the current txs.
// We only need to create/revert checkpoint here because of this addNewContracts call.
- await this.contractsDB.addNewContracts(tx);
+ this.contractsDB.addNewContracts(tx);
if (!tx.data.forPublic) {
this.#log.debug(
diff --git a/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts b/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts
index 73234f5cc6a3..a7aea045ff8d 100644
--- a/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts
+++ b/yarn-project/protocol-contracts/src/class-registry/contract_class_published_event.ts
@@ -4,7 +4,7 @@ import { FieldReader } from '@aztec/foundation/serialize';
import { bufferFromFields } from '@aztec/stdlib/abi';
import {
type ContractClassPublic,
- computeContractClassId,
+ type ContractClassPublicWithCommitment,
computePublicBytecodeCommitment,
} from '@aztec/stdlib/contract';
import type { ContractClassLog } from '@aztec/stdlib/logs';
@@ -47,34 +47,25 @@ export class ContractClassPublishedEvent {
);
}
- async toContractClassPublic(): Promise {
- const computedClassId = await computeContractClassId({
- artifactHash: this.artifactHash,
- privateFunctionsRoot: this.privateFunctionsRoot,
- publicBytecodeCommitment: await computePublicBytecodeCommitment(this.packedPublicBytecode),
- });
-
- if (!computedClassId.equals(this.contractClassId)) {
- throw new Error(
- `Invalid contract class id: computed ${computedClassId.toString()} but event broadcasted ${this.contractClassId.toString()}`,
- );
- }
-
- if (this.version !== 1) {
- throw new Error(`Unexpected contract class version ${this.version}`);
- }
-
+ /** Converts the event to a contract class, without computing or validating the bytecode commitment. */
+ toContractClassPublic(): ContractClassPublic {
return {
id: this.contractClassId,
artifactHash: this.artifactHash,
packedBytecode: this.packedPublicBytecode,
privateFunctionsRoot: this.privateFunctionsRoot,
- version: this.version,
+ version: this.version as 1,
privateFunctions: [],
utilityFunctions: [],
};
}
+ /** Converts the event to a contract class with its bytecode commitment (expensive). */
+ async toContractClassPublicWithBytecodeCommitment(): Promise {
+ const publicBytecodeCommitment = await computePublicBytecodeCommitment(this.packedPublicBytecode);
+ return { ...this.toContractClassPublic(), publicBytecodeCommitment };
+ }
+
public static extractContractClassEvents(logs: ContractClassLog[]): ContractClassPublishedEvent[] {
return logs
.filter((log: ContractClassLog) => ContractClassPublishedEvent.isContractClassPublishedEvent(log))
diff --git a/yarn-project/simulator/src/public/public_db_sources.ts b/yarn-project/simulator/src/public/public_db_sources.ts
index 445949934694..144a39182563 100644
--- a/yarn-project/simulator/src/public/public_db_sources.ts
+++ b/yarn-project/simulator/src/public/public_db_sources.ts
@@ -55,10 +55,10 @@ export class PublicContractsDB implements PublicContractsDBInterface {
this.log = createLogger('simulator:contracts-data-source', bindings);
}
- public async addContracts(contractDeploymentData: ContractDeploymentData): Promise {
+ public addContracts(contractDeploymentData: ContractDeploymentData): void {
const currentState = this.getCurrentState();
- await this.addContractClassesFromEvents(
+ this.addContractClassesFromEvents(
ContractClassPublishedEvent.extractContractClassEvents(contractDeploymentData.getContractClassLogs()),
currentState,
);
@@ -69,10 +69,10 @@ export class PublicContractsDB implements PublicContractsDBInterface {
);
}
- public async addNewContracts(tx: Tx): Promise {
+ public addNewContracts(tx: Tx): void {
const contractDeploymentData = AllContractDeploymentData.fromTx(tx);
- await this.addContracts(contractDeploymentData.getNonRevertibleContractDeploymentData());
- await this.addContracts(contractDeploymentData.getRevertibleContractDeploymentData());
+ this.addContracts(contractDeploymentData.getNonRevertibleContractDeploymentData());
+ this.addContracts(contractDeploymentData.getRevertibleContractDeploymentData());
}
/**
@@ -174,17 +174,15 @@ export class PublicContractsDB implements PublicContractsDBInterface {
return await this.dataSource.getDebugFunctionName(address, selector);
}
- private async addContractClassesFromEvents(
+ private addContractClassesFromEvents(
contractClassEvents: ContractClassPublishedEvent[],
state: ContractsDbCheckpoint,
) {
- await Promise.all(
- contractClassEvents.map(async (event: ContractClassPublishedEvent) => {
- this.log.debug(`Adding class ${event.contractClassId.toString()} to contract state`);
- const contractClass = await event.toContractClassPublic();
- state.addClass(event.contractClassId, contractClass);
- }),
- );
+ for (const event of contractClassEvents) {
+ this.log.debug(`Adding class ${event.contractClassId.toString()} to contract state`);
+ const contractClass = event.toContractClassPublic();
+ state.addClass(event.contractClassId, contractClass);
+ }
}
private addContractInstancesFromEvents(
diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor/public_processor.test.ts
index 23a019bb6080..abc3aedf918e 100644
--- a/yarn-project/simulator/src/public/public_processor/public_processor.test.ts
+++ b/yarn-project/simulator/src/public/public_processor/public_processor.test.ts
@@ -361,8 +361,8 @@ describe('public_processor', () => {
// we want to confirm that even non-revertibles get cleared
const contractClassId = await mockContractClassForTx(tx, /*revertible=*/ false);
- publicTxSimulator.simulate.mockImplementation(async (simulatedTx: Tx) => {
- await contractsDB.addNewContracts(simulatedTx);
+ publicTxSimulator.simulate.mockImplementation((simulatedTx: Tx) => {
+ contractsDB.addNewContracts(simulatedTx);
throw new Error('Uncaught error');
});
diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts
index f8b708d0a568..29a4f6aaadbf 100644
--- a/yarn-project/simulator/src/public/public_processor/public_processor.ts
+++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts
@@ -548,7 +548,7 @@ export class PublicProcessor implements Traceable {
// Fee payment insertion has already been done. Do the rest.
await this.doTreeInsertionsForPrivateOnlyTx(processedTx);
- await this.contractsDB.addNewContracts(tx);
+ this.contractsDB.addNewContracts(tx);
return [processedTx, undefined, []];
}
diff --git a/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts b/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts
index 6568a47228e9..9077b8676c1e 100644
--- a/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts
+++ b/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts
@@ -52,6 +52,7 @@ export class ContractProviderForCpp implements ContractProvider {
return serializeWithMessagePack(contractClass);
};
+ // eslint-disable-next-line require-await
public addContracts = async (contractDeploymentDataBuffer: Buffer): Promise => {
this.log.trace(`Contract provider callback: addContracts`);
@@ -62,7 +63,7 @@ export class ContractProviderForCpp implements ContractProvider {
// Add contracts to the contracts DB
this.log.trace(`Calling contractsDB.addContracts`);
- await this.contractsDB.addContracts(contractDeploymentData);
+ this.contractsDB.addContracts(contractDeploymentData);
};
public getBytecodeCommitment = async (classId: string): Promise => {
diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts
index c07f0d04945c..4e2b95c21c7c 100644
--- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts
+++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts
@@ -401,7 +401,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
// However, things work as expected because later calls to getters on the hintingContractsDB
// will pick up the new contracts and will generate the necessary hints.
// So, a consumer of the hints will always see the new contracts.
- await this.contractsDB.addContracts(context.nonRevertibleContractDeploymentData);
+ this.contractsDB.addContracts(context.nonRevertibleContractDeploymentData);
}
/**
@@ -486,7 +486,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
// However, things work as expected because later calls to getters on the hintingContractsDB
// will pick up the new contracts and will generate the necessary hints.
// So, a consumer of the hints will always see the new contracts.
- await this.contractsDB.addContracts(context.revertibleContractDeploymentData);
+ this.contractsDB.addContracts(context.revertibleContractDeploymentData);
}
private async payFee(context: PublicTxContext) {
diff --git a/yarn-project/stdlib/src/contract/contract_class_id.test.ts b/yarn-project/stdlib/src/contract/contract_class_id.test.ts
index 224957aa6431..04df3321ab02 100644
--- a/yarn-project/stdlib/src/contract/contract_class_id.test.ts
+++ b/yarn-project/stdlib/src/contract/contract_class_id.test.ts
@@ -1,6 +1,10 @@
import { Fr } from '@aztec/foundation/curves/bn254';
+import { createLogger } from '@aztec/foundation/log';
+import { elapsed } from '@aztec/foundation/timer';
import { FunctionSelector } from '../abi/function_selector.js';
+import { getBenchmarkContractArtifact, getTestContractArtifact, getTokenContractArtifact } from '../tests/fixtures.js';
+import { getContractClassFromArtifact } from './contract_class.js';
import { computeContractClassId } from './contract_class_id.js';
import type { ContractClass } from './interfaces/contract_class.js';
@@ -18,5 +22,20 @@ describe('ContractClass', () => {
`"0x2926577ccab09f8e4600550792066ed9d6ce530a973ac2b81a36eaebee56ad44"`,
);
});
+
+ it('calculates the contract class id for a real contract artifact', async () => {
+ const artifacts = [getBenchmarkContractArtifact(), getTokenContractArtifact(), getTestContractArtifact()];
+ const logger = createLogger('stdlib:contract_class_id:test');
+
+ for (const artifact of artifacts) {
+ const contractClass = await getContractClassFromArtifact(artifact);
+
+ const [ms, contractClassId] = await elapsed(computeContractClassId(contractClass));
+ logger.info(`Computed contract class id ${contractClassId} in ${ms}ms`);
+
+ expect(contractClassId.toString()).toHaveLength(66); // 0x + 64 hex chars
+ expect(contractClassId.toBigInt()).toBeGreaterThan(0n);
+ }
+ });
});
});
diff --git a/yarn-project/stdlib/src/tx/validator/error_texts.ts b/yarn-project/stdlib/src/tx/validator/error_texts.ts
index 6a8326f032a8..bcc100c2d9b0 100644
--- a/yarn-project/stdlib/src/tx/validator/error_texts.ts
+++ b/yarn-project/stdlib/src/tx/validator/error_texts.ts
@@ -41,5 +41,13 @@ export const TX_ERROR_SIZE_ABOVE_LIMIT = 'Transaction size above size limit';
// Block header
export const TX_ERROR_BLOCK_HEADER = 'Block header not found';
+// Contract instance
+export const TX_ERROR_INCORRECT_CONTRACT_ADDRESS = 'Incorrect contract instance deployment address';
+export const TX_ERROR_MALFORMED_CONTRACT_INSTANCE_LOG = 'Failed to parse contract instance deployment log';
+
+// Contract class
+export const TX_ERROR_INCORRECT_CONTRACT_CLASS_ID = 'Incorrect contract class id';
+export const TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG = 'Failed to parse contract class registration log';
+
// General
export const TX_ERROR_DURING_VALIDATION = 'Unexpected error during validation';
From e82ca40667d272b79b77baa31e1e4a93252ce3fa Mon Sep 17 00:00:00 2001
From: Gregorio Juliana
Date: Fri, 20 Mar 2026 19:00:22 +0100
Subject: [PATCH 10/10] feat: sync poseidon browser (#21851)
Broadens the check to ensure the sync version is used accross the
browser, service workers, web workers and extension contexts.
---
yarn-project/foundation/src/crypto/poseidon/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yarn-project/foundation/src/crypto/poseidon/index.ts b/yarn-project/foundation/src/crypto/poseidon/index.ts
index adbd7a4b4eb4..e5cf4ded640e 100644
--- a/yarn-project/foundation/src/crypto/poseidon/index.ts
+++ b/yarn-project/foundation/src/crypto/poseidon/index.ts
@@ -3,7 +3,7 @@ import { Barretenberg, BarretenbergSync } from '@aztec/bb.js';
import { Fr } from '../../curves/bn254/field.js';
import { type Fieldable, serializeToFields } from '../../serialize/serialize.js';
-const IS_BROWSER = typeof window !== 'undefined';
+const IS_BROWSER = typeof self !== 'undefined';
async function poseidon2HashFields(inputFields: Fr[]): Promise {
if (IS_BROWSER) {