Skip to content

Commit ffef283

Browse files
authored
Merge branch 'backport-to-v4-next-staging' into claudebox/backport-22872-pxe-db-schema-test
2 parents 07ae74c + 381d7bb commit ffef283

17 files changed

Lines changed: 175 additions & 10 deletions

File tree

noir-projects/noir-contracts/bootstrap.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ function get_contract_path {
9393
}
9494
export -f get_contract_path
9595

96+
# Stamps the aztec version into a contract artifact JSON in place. Mirrors stampAztecVersion in
97+
# yarn-project/aztec/src/cli/cmds/compile.ts so monorepo-built artifacts match those produced by `aztec compile`.
98+
# On release builds (REF_NAME is valid semver) the tag without the leading "v" is used; otherwise "dev".
99+
function stamp_aztec_version {
100+
local json_path=$1
101+
# "dev" here corresponds to DEV_VERSION in yarn-project/stdlib/src/update-checker/dev_version.ts.
102+
local version="dev"
103+
semver check "$REF_NAME" 2>/dev/null && version="${REF_NAME#v}"
104+
local tmp=$(mktemp)
105+
jq --arg v "$version" '.aztec_version = $v' "$json_path" > "$tmp"
106+
mv "$tmp" "$json_path"
107+
}
108+
export -f stamp_aztec_version
109+
96110
# This compiles a noir contract, transpiles public functions, strips internal prefixes,
97111
# and generates verification keys for private functions via 'bb aztec_process'.
98112
# $1 is the input package name, $2 is the folder name (e.g. "contracts" or "examples")
@@ -113,6 +127,9 @@ function compile {
113127
$BB aztec_process -i $json_path
114128
cache_upload contract-$contract_hash.tar.gz $json_path
115129
fi
130+
# Stamp the current version after the cache block so the field always matches the build's version, whether
131+
# the artifact came from a fresh compile or a cache hit.
132+
stamp_aztec_version "$json_path"
116133
}
117134
export -f compile
118135

yarn-project/aztec.js/src/contract/contract.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@aztec/stdlib/contract';
99
import type { TxExecutionRequest, TxReceipt, UtilityExecutionResult } from '@aztec/stdlib/tx';
1010
import { OFFCHAIN_MESSAGE_IDENTIFIER } from '@aztec/stdlib/tx';
11+
import { DEV_VERSION } from '@aztec/stdlib/update-checker';
1112

1213
import { type MockProxy, mock } from 'jest-mock-extended';
1314

@@ -37,6 +38,7 @@ describe('Contract Class', () => {
3738

3839
const defaultArtifact: ContractArtifact = {
3940
name: 'FooContract',
41+
aztecVersion: DEV_VERSION,
4042
functions: [
4143
{
4244
name: 'bar',

yarn-project/aztec.js/src/contract/deploy_method.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract';
55
import { Gas } from '@aztec/stdlib/gas';
66
import { PublicKeys } from '@aztec/stdlib/keys';
77
import { OFFCHAIN_MESSAGE_IDENTIFIER, type OffchainEffect } from '@aztec/stdlib/tx';
8+
import { DEV_VERSION } from '@aztec/stdlib/update-checker';
89

910
import { type MockProxy, mock } from 'jest-mock-extended';
1011

@@ -18,6 +19,7 @@ describe('DeployMethod', () => {
1819

1920
const artifact: ContractArtifact = {
2021
name: 'TestContract',
22+
aztecVersion: DEV_VERSION,
2123
functions: [
2224
{
2325
name: 'constructor',

yarn-project/aztec.js/src/scripts/generate_protocol_contract_types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function generateProtocolContractArtifact(input: ContractArtifact): string {
6363

6464
return `{
6565
name: '${input.name}',
66+
aztecVersion: '${input.aztecVersion}',
6667
functions: [
6768
${functionsArray}
6869
],

yarn-project/aztec.js/src/wallet/wallet.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
TxSimulationResult,
1919
UtilityExecutionResult,
2020
} from '@aztec/stdlib/tx';
21+
import { DEV_VERSION } from '@aztec/stdlib/update-checker';
2122

2223
import {
2324
type InteractionWaitOptions,
@@ -126,6 +127,7 @@ describe('WalletSchema', () => {
126127
it('registerContract', async () => {
127128
const mockArtifact: ContractArtifact = {
128129
name: 'TestContract',
130+
aztecVersion: DEV_VERSION,
129131
functions: [],
130132
nonDispatchPublicFunctions: [],
131133
outputs: { structs: {}, globals: {} },
@@ -318,6 +320,7 @@ describe('WalletSchema', () => {
318320

319321
const mockArtifact: ContractArtifact = {
320322
name: 'TestContract',
323+
aztecVersion: DEV_VERSION,
321324
functions: [],
322325
nonDispatchPublicFunctions: [],
323326
outputs: { structs: {}, globals: {} },

yarn-project/aztec/src/cli/cmds/compile.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { findBbBinary } from '@aztec/bb.js';
22
import type { LogFn } from '@aztec/foundation/log';
3+
import { getPackageVersion } from '@aztec/stdlib/update-checker';
34

45
import { execFileSync } from 'child_process';
56
import type { Command } from 'commander';
6-
import { readFile } from 'fs/promises';
7+
import { readFile, writeFile } from 'fs/promises';
78
import { join } from 'path';
89

910
import { readArtifactFiles } from './utils/artifacts.js';
@@ -25,6 +26,17 @@ async function collectContractArtifacts(): Promise<string[]> {
2526
return files.filter(f => Array.isArray(f.content.functions)).map(f => f.filePath);
2627
}
2728

29+
/** Stamps the Aztec stack version into the contract artifacts. */
30+
async function stampAztecVersion(artifactPaths: string[]): Promise<void> {
31+
const version = getPackageVersion();
32+
for (const path of artifactPaths) {
33+
const artifact = JSON.parse(await readFile(path, 'utf-8'));
34+
// eslint-disable-next-line camelcase
35+
artifact.aztec_version = version;
36+
await writeFile(path, JSON.stringify(artifact, null, 2) + '\n');
37+
}
38+
}
39+
2840
/** Returns the set of package names that are contract crates in the current workspace. */
2941
async function getContractPackageNames(): Promise<Set<string>> {
3042
const contractNames = new Set<string>();
@@ -148,6 +160,8 @@ async function compileAztecContract(nargoArgs: string[], log: LogFn): Promise<vo
148160
log('Postprocessing contracts...');
149161
const bbArgs = artifacts.flatMap(a => ['-i', a]);
150162
await run(bb, ['aztec_process', ...bbArgs]);
163+
164+
await stampAztecVersion(artifacts);
151165
}
152166

153167
log('Compilation complete!');

yarn-project/end-to-end/src/fixtures/setup.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { protocolContractsHash } from '@aztec/protocol-contracts';
4747
import type { ProverNodeConfig } from '@aztec/prover-node';
4848
import { type PXEConfig, getPXEConfig } from '@aztec/pxe/server';
4949
import type { SequencerClient } from '@aztec/sequencer-client';
50+
import { ARTIFACT_VERSION_BEFORE_INJECTION } from '@aztec/stdlib/abi';
5051
import { type ContractInstanceWithAddress, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
5152
import type { AztecNodeAdmin, AztecNodeDebug } from '@aztec/stdlib/interfaces/client';
5253
import { tryStop } from '@aztec/stdlib/interfaces/server';
@@ -260,6 +261,32 @@ export type EndToEndContext = {
260261
teardown: () => Promise<void>;
261262
};
262263

264+
/**
265+
* When CONTRACT_ARTIFACTS_VERSION is set (backwards compatibility testing), asserts that the loaded artifact's
266+
* aztecVersion matches the expected version. This is a sanity check verifying that the legacy artifact resolver
267+
* actually swapped in the correct version.
268+
*/
269+
function assertContractArtifactsVersion() {
270+
const expected = process.env.CONTRACT_ARTIFACTS_VERSION;
271+
if (!expected) {
272+
return;
273+
}
274+
const { aztecVersion } = SponsoredFPCContract.artifact;
275+
// TODO(F-557): Remove this bypass once pre-version artifacts are no longer tested.
276+
if (aztecVersion === ARTIFACT_VERSION_BEFORE_INJECTION) {
277+
createLogger('e2e:setup').info(
278+
`Skipping artifact version check: artifact predates version injection (CONTRACT_ARTIFACTS_VERSION=${expected})`,
279+
);
280+
return;
281+
}
282+
if (aztecVersion !== expected) {
283+
throw new Error(
284+
`Artifact version mismatch: expected ${expected} but got ${aztecVersion}. ` +
285+
`The legacy artifact resolver may not have swapped in the correct version.`,
286+
);
287+
}
288+
}
289+
263290
/**
264291
* Sets up the environment for the end-to-end tests.
265292
* @param numberOfAccounts - The number of new accounts to be created once the PXE is initiated.
@@ -272,6 +299,7 @@ export async function setup(
272299
pxeOpts: Partial<PXEConfig> = {},
273300
chain: Chain = foundry,
274301
): Promise<EndToEndContext> {
302+
assertContractArtifactsVersion();
275303
let anvil: Anvil | undefined;
276304
try {
277305
opts.aztecTargetCommitteeSize ??= 0;

yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,53 @@ describe('ContractSyncService', () => {
225225
});
226226
});
227227

228+
describe('class ID verification deduplication', () => {
229+
const contract2 = AztecAddress.fromBigInt(300n);
230+
231+
it('verifies class ID only once per contract across scope batches', async () => {
232+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
233+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeB]);
234+
expectVerifiedContracts(contractAddress);
235+
});
236+
237+
it('verifies class ID separately for different contracts', async () => {
238+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
239+
await service.ensureContractSynced(contract2, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
240+
expectVerifiedContracts(contractAddress, contract2);
241+
});
242+
243+
it('re-verifies class ID after wipe', async () => {
244+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
245+
service.wipe();
246+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeB]);
247+
expectVerifiedContracts(contractAddress, contractAddress);
248+
});
249+
250+
it('re-verifies class ID after discardStaged', async () => {
251+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
252+
await service.discardStaged(jobId);
253+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
254+
expectVerifiedContracts(contractAddress, contractAddress);
255+
});
256+
257+
it('re-verifies class ID after verification failure', async () => {
258+
contractStore.getContractInstance.mockRejectedValueOnce(new Error('node unavailable'));
259+
await expect(
260+
service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]),
261+
).rejects.toThrow('node unavailable');
262+
263+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
264+
expectVerifiedContracts(contractAddress, contractAddress);
265+
});
266+
267+
it('does not re-verify class ID when only scope cache is invalidated', async () => {
268+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
269+
service.invalidateContractForScopes(contractAddress, [scopeA]);
270+
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
271+
expectVerifiedContracts(contractAddress);
272+
});
273+
});
274+
228275
describe('invalidateContractForScopes', () => {
229276
const contract2 = AztecAddress.fromBigInt(300n);
230277

@@ -337,5 +384,13 @@ describe('ContractSyncService', () => {
337384
}
338385
};
339386

387+
/** Asserts that class ID verification was triggered for each contract address in the given sequence. */
388+
const expectVerifiedContracts = (...addresses: AztecAddress[]) => {
389+
expect(contractStore.getContractInstance).toHaveBeenCalledTimes(addresses.length);
390+
for (let i = 0; i < addresses.length; i++) {
391+
expect(contractStore.getContractInstance).toHaveBeenNthCalledWith(i + 1, addresses[i]);
392+
}
393+
};
394+
340395
const expectNoSync = () => expect(utilityExecutor).not.toHaveBeenCalled();
341396
});

yarn-project/pxe/src/contract_sync/contract_sync_service.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export class ContractSyncService implements StagedStore {
2727
// The value is a promise that resolves when the contract is synced.
2828
private syncedContracts: Map<string, Promise<void>> = new Map();
2929

30+
// Tracks class ID verification per contract. Keyed by contract address only (no scope), since
31+
// class ID verification is scope-independent. Cleared on wipe/discard.
32+
private verifiedClassIds: Map<string, Promise<void>> = new Map();
33+
3034
// Per-job excluded contract addresses - these contracts should not be synced.
3135
private excludedFromSync: Map<string, Set<string>> = new Map();
3236

@@ -101,6 +105,7 @@ export class ContractSyncService implements StagedStore {
101105
wipe(): void {
102106
this.log.debug(`Wiping contract sync cache (${this.syncedContracts.size} entries)`);
103107
this.syncedContracts.clear();
108+
this.verifiedClassIds.clear();
104109
}
105110

106111
commit(jobId: string): Promise<void> {
@@ -113,6 +118,7 @@ export class ContractSyncService implements StagedStore {
113118
// We clear the synced contracts cache here because, when the job is discarded, any associated database writes from
114119
// the sync are also undone.
115120
this.syncedContracts.clear();
121+
this.verifiedClassIds.clear();
116122
this.excludedFromSync.delete(jobId);
117123
return Promise.resolve();
118124
}
@@ -138,7 +144,7 @@ export class ContractSyncService implements StagedStore {
138144
}
139145

140146
this.log.debug(`Syncing contract ${contractAddress} for ${scopesToSync.length} scope(s)`);
141-
const verifyPromise = verifyFn();
147+
const verifyPromise = this.#getOrStartVerification(contractAddress, verifyFn);
142148

143149
for (const scope of scopesToSync) {
144150
const key = toKey(contractAddress, scope);
@@ -152,6 +158,21 @@ export class ContractSyncService implements StagedStore {
152158
}
153159
}
154160

161+
/** Returns the cached verification promise for a contract, starting a new one if needed. Evicts from cache on failure so retries re-verify. */
162+
#getOrStartVerification(contractAddress: AztecAddress, verifyFn: () => Promise<void>): Promise<void> {
163+
const contractKey = contractAddress.toString();
164+
const cached = this.verifiedClassIds.get(contractKey);
165+
if (cached) {
166+
return cached;
167+
}
168+
const promise = verifyFn().catch(err => {
169+
this.verifiedClassIds.delete(contractKey);
170+
throw err;
171+
});
172+
this.verifiedClassIds.set(contractKey, promise);
173+
return promise;
174+
}
175+
155176
/** Runs fn while holding a slot in #syncSlot, bounding total concurrent scope syncs. */
156177
async #runBounded<T>(fn: () => Promise<T>): Promise<T> {
157178
await this.#syncSlot.acquire();

yarn-project/stdlib/src/abi/abi.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { schemas, zodFor } from '@aztec/foundation/schemas';
66
import { inflate } from 'pako';
77
import { z } from 'zod';
88

9+
import { DEV_VERSION } from '../update-checker/dev_version.js';
910
import { FunctionSelector } from './function_selector.js';
1011

1112
/** A basic value. */
@@ -384,11 +385,17 @@ export type FieldLayout = {
384385
slot: Fr;
385386
};
386387

388+
/** Placeholder version injected into artifacts compiled before aztecVersion was added. TODO(F-557): Remove. */
389+
export const ARTIFACT_VERSION_BEFORE_INJECTION = 'FROM_RELEASE_BEFORE_VERSION_INJECTION';
390+
387391
/** Defines artifact of a contract. */
388392
export interface ContractArtifact {
389393
/** The name of the contract. */
390394
name: string;
391395

396+
/** The version of the Aztec stack that compiled this artifact. */
397+
aztecVersion: string;
398+
392399
/** The functions of the contract. Includes private and utility functions, plus the public dispatch function. */
393400
functions: FunctionArtifact[];
394401

@@ -411,6 +418,7 @@ export interface ContractArtifact {
411418
export const ContractArtifactSchema = zodFor<ContractArtifact>()(
412419
z.object({
413420
name: z.string(),
421+
aztecVersion: z.string().default(ARTIFACT_VERSION_BEFORE_INJECTION), // TODO(F-557): Remove default.
414422
functions: z.array(FunctionArtifactSchema),
415423
nonDispatchPublicFunctions: z.array(FunctionAbiSchema),
416424
outputs: z.object({
@@ -604,6 +612,7 @@ export function emptyFunctionArtifact(): FunctionArtifact {
604612
export function emptyContractArtifact(): ContractArtifact {
605613
return {
606614
name: '',
615+
aztecVersion: DEV_VERSION,
607616
functions: [emptyFunctionArtifact()],
608617
nonDispatchPublicFunctions: [emptyFunctionAbi()],
609618
outputs: {

0 commit comments

Comments
 (0)