@aztec/end-to-end 0.0.1-commit.29c6b1a3 → 0.0.1-commit.2e2504e2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dest/e2e_cross_chain_messaging/cross_chain_messaging_test.d.ts +1 -1
  2. package/dest/e2e_cross_chain_messaging/cross_chain_messaging_test.d.ts.map +1 -1
  3. package/dest/e2e_cross_chain_messaging/cross_chain_messaging_test.js +3 -2
  4. package/dest/e2e_epochs/epochs_test.d.ts +1 -1
  5. package/dest/e2e_epochs/epochs_test.d.ts.map +1 -1
  6. package/dest/e2e_epochs/epochs_test.js +11 -6
  7. package/dest/e2e_l1_publisher/write_json.d.ts +3 -2
  8. package/dest/e2e_l1_publisher/write_json.d.ts.map +1 -1
  9. package/dest/e2e_l1_publisher/write_json.js +1 -7
  10. package/dest/e2e_p2p/reqresp/utils.d.ts +22 -0
  11. package/dest/e2e_p2p/reqresp/utils.d.ts.map +1 -0
  12. package/dest/e2e_p2p/reqresp/utils.js +153 -0
  13. package/dest/e2e_p2p/shared.d.ts +1 -1
  14. package/dest/e2e_p2p/shared.d.ts.map +1 -1
  15. package/dest/e2e_p2p/shared.js +2 -2
  16. package/dest/fixtures/e2e_prover_test.js +1 -1
  17. package/dest/fixtures/ha_setup.d.ts +71 -0
  18. package/dest/fixtures/ha_setup.d.ts.map +1 -0
  19. package/dest/fixtures/ha_setup.js +114 -0
  20. package/dest/fixtures/index.d.ts +2 -1
  21. package/dest/fixtures/index.d.ts.map +1 -1
  22. package/dest/fixtures/index.js +1 -0
  23. package/dest/fixtures/setup.d.ts +3 -3
  24. package/dest/fixtures/setup.d.ts.map +1 -1
  25. package/dest/fixtures/setup.js +19 -14
  26. package/dest/fixtures/setup_p2p_test.d.ts +4 -5
  27. package/dest/fixtures/setup_p2p_test.d.ts.map +1 -1
  28. package/dest/fixtures/setup_p2p_test.js +24 -19
  29. package/dest/shared/uniswap_l1_l2.d.ts +1 -1
  30. package/dest/shared/uniswap_l1_l2.d.ts.map +1 -1
  31. package/dest/shared/uniswap_l1_l2.js +7 -5
  32. package/dest/spartan/utils/config.d.ts +4 -1
  33. package/dest/spartan/utils/config.d.ts.map +1 -1
  34. package/dest/spartan/utils/config.js +2 -1
  35. package/dest/spartan/utils/index.d.ts +4 -4
  36. package/dest/spartan/utils/index.d.ts.map +1 -1
  37. package/dest/spartan/utils/index.js +2 -2
  38. package/dest/spartan/utils/k8s.d.ts +29 -1
  39. package/dest/spartan/utils/k8s.d.ts.map +1 -1
  40. package/dest/spartan/utils/k8s.js +118 -0
  41. package/dest/spartan/utils/nodes.d.ts +11 -1
  42. package/dest/spartan/utils/nodes.d.ts.map +1 -1
  43. package/dest/spartan/utils/nodes.js +198 -27
  44. package/dest/spartan/utils/scripts.d.ts +18 -4
  45. package/dest/spartan/utils/scripts.d.ts.map +1 -1
  46. package/dest/spartan/utils/scripts.js +19 -4
  47. package/package.json +42 -39
  48. package/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +3 -4
  49. package/src/e2e_epochs/epochs_test.ts +7 -6
  50. package/src/e2e_l1_publisher/write_json.ts +1 -6
  51. package/src/e2e_p2p/reqresp/utils.ts +207 -0
  52. package/src/e2e_p2p/shared.ts +10 -2
  53. package/src/fixtures/dumps/epoch_proof_result.json +1 -1
  54. package/src/fixtures/e2e_prover_test.ts +1 -1
  55. package/src/fixtures/ha_setup.ts +184 -0
  56. package/src/fixtures/index.ts +1 -0
  57. package/src/fixtures/setup.ts +12 -12
  58. package/src/fixtures/setup_p2p_test.ts +15 -20
  59. package/src/shared/uniswap_l1_l2.ts +7 -9
  60. package/src/spartan/utils/config.ts +1 -0
  61. package/src/spartan/utils/index.ts +3 -1
  62. package/src/spartan/utils/k8s.ts +152 -0
  63. package/src/spartan/utils/nodes.ts +239 -24
  64. package/src/spartan/utils/scripts.ts +43 -7
@@ -198,7 +198,7 @@ export class FullProverTest {
198
198
  this.aztecNode,
199
199
  { proverEnabled: this.realProofs },
200
200
  undefined,
201
- true,
201
+ 'pxe-proven',
202
202
  );
203
203
  this.logger.debug(`Contract address ${this.fakeProofsAsset.address}`);
204
204
  await provenWallet.registerContract(this.fakeProofsAssetInstance, TokenContract.artifact);
@@ -0,0 +1,184 @@
1
+ import { EthAddress } from '@aztec/aztec.js/addresses';
2
+ import { Fr } from '@aztec/aztec.js/fields';
3
+ import type { Logger } from '@aztec/aztec.js/log';
4
+ import { SecretValue } from '@aztec/foundation/config';
5
+
6
+ import { Pool } from 'pg';
7
+ import { privateKeyToAccount } from 'viem/accounts';
8
+
9
+ /**
10
+ * Configuration for HA database connection
11
+ */
12
+ export interface HADatabaseConfig {
13
+ /** PostgreSQL connection URL */
14
+ databaseUrl: string;
15
+ /** Node ID for HA coordination */
16
+ nodeId: string;
17
+ /** Enable HA signing */
18
+ haSigningEnabled: boolean;
19
+ /** Polling interval in ms */
20
+ pollingIntervalMs: number;
21
+ /** Signing timeout in ms */
22
+ signingTimeoutMs: number;
23
+ /** Max stuck duties age in ms */
24
+ maxStuckDutiesAgeMs: number;
25
+ }
26
+
27
+ /**
28
+ * Get database configuration from environment variables
29
+ */
30
+ export function createHADatabaseConfig(nodeId: string): HADatabaseConfig {
31
+ const databaseUrl = process.env.DATABASE_URL || 'postgresql://aztec:aztec@localhost:5432/aztec_ha_test';
32
+
33
+ return {
34
+ databaseUrl,
35
+ nodeId,
36
+ haSigningEnabled: true,
37
+ pollingIntervalMs: 100,
38
+ signingTimeoutMs: 3000,
39
+ maxStuckDutiesAgeMs: 72000,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Setup PostgreSQL database connection pool for HA tests
45
+ *
46
+ * Note: Database migrations should be run separately before starting tests,
47
+ * either via docker-compose entrypoint or manually with: aztec migrate-ha-db up
48
+ */
49
+ export function setupHADatabase(databaseUrl: string, logger?: Logger): Pool {
50
+ try {
51
+ // Create connection pool for test usage
52
+ // Migrations are already run by docker-compose entrypoint before tests start
53
+ const pool = new Pool({ connectionString: databaseUrl });
54
+
55
+ logger?.info('Connected to HA database (migrations should already be applied)');
56
+
57
+ return pool;
58
+ } catch (error) {
59
+ logger?.error(`Failed to connect to HA database: ${error}`);
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Clean up HA database - drop all tables
66
+ * Use this between tests to ensure clean state
67
+ */
68
+ export async function cleanupHADatabase(pool: Pool, logger?: Logger): Promise<void> {
69
+ try {
70
+ // Drop all HA tables
71
+ await pool.query('DROP TABLE IF EXISTS validator_duties CASCADE');
72
+ await pool.query('DROP TABLE IF EXISTS slashing_protection CASCADE');
73
+ await pool.query('DROP TABLE IF EXISTS schema_version CASCADE');
74
+
75
+ logger?.info('HA database cleaned up successfully');
76
+ } catch (error) {
77
+ logger?.error(`Failed to cleanup HA database: ${error}`);
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Query validator duties from the database
84
+ */
85
+ export async function getValidatorDuties(
86
+ pool: Pool,
87
+ slot: bigint,
88
+ dutyType?: 'ATTESTATION' | 'BLOCK_PROPOSAL' | 'GOVERNANCE_VOTE' | 'SLASHING_VOTE',
89
+ ): Promise<
90
+ Array<{
91
+ slot: string;
92
+ dutyType: string;
93
+ validatorAddress: string;
94
+ nodeId: string;
95
+ startedAt: Date;
96
+ completedAt: Date | undefined;
97
+ }>
98
+ > {
99
+ const query = dutyType
100
+ ? 'SELECT slot, duty_type, validator_address, node_id, started_at, completed_at FROM validator_duties WHERE slot = $1 AND duty_type = $2 ORDER BY started_at'
101
+ : 'SELECT slot, duty_type, validator_address, node_id, started_at, completed_at FROM validator_duties WHERE slot = $1 ORDER BY started_at';
102
+
103
+ const params = dutyType ? [slot.toString(), dutyType] : [slot.toString()];
104
+
105
+ const result = await pool.query<{
106
+ slot: string;
107
+ duty_type: string;
108
+ validator_address: string;
109
+ node_id: string;
110
+ started_at: Date;
111
+ completed_at: Date | undefined;
112
+ }>(query, params);
113
+
114
+ return result.rows.map(row => ({
115
+ slot: row.slot,
116
+ dutyType: row.duty_type,
117
+ validatorAddress: row.validator_address,
118
+ nodeId: row.node_id,
119
+ startedAt: row.started_at,
120
+ completedAt: row.completed_at,
121
+ }));
122
+ }
123
+
124
+ /**
125
+ * Convert private keys to Ethereum addresses
126
+ */
127
+ export function getAddressesFromPrivateKeys(privateKeys: `0x${string}`[]): string[] {
128
+ return privateKeys.map(pk => {
129
+ const account = privateKeyToAccount(pk);
130
+ return account.address;
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Create initial validators from private keys for L1 contract deployment
136
+ */
137
+ export function createInitialValidatorsFromPrivateKeys(attesterPrivateKeys: `0x${string}`[]): Array<{
138
+ attester: EthAddress;
139
+ withdrawer: EthAddress;
140
+ privateKey: `0x${string}`;
141
+ bn254SecretKey: SecretValue<bigint>;
142
+ }> {
143
+ return attesterPrivateKeys.map(pk => {
144
+ const account = privateKeyToAccount(pk);
145
+ return {
146
+ attester: EthAddress.fromString(account.address),
147
+ withdrawer: EthAddress.fromString(account.address),
148
+ privateKey: pk,
149
+ bn254SecretKey: new SecretValue(Fr.random().toBigInt()),
150
+ };
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Verify no duplicate attestations per validator (HA coordination check)
156
+ * Groups duties by validator address and verifies each validator attested exactly once
157
+ */
158
+ export function verifyNoDuplicateAttestations(
159
+ attestationDuties: Array<{
160
+ validatorAddress: string;
161
+ nodeId: string;
162
+ completedAt: Date | undefined;
163
+ }>,
164
+ logger?: Logger,
165
+ ): Map<string, typeof attestationDuties> {
166
+ const dutiesByValidator = new Map<string, typeof attestationDuties>();
167
+ for (const duty of attestationDuties) {
168
+ const existing = dutiesByValidator.get(duty.validatorAddress) || [];
169
+ existing.push(duty);
170
+ dutiesByValidator.set(duty.validatorAddress, existing);
171
+ }
172
+
173
+ for (const [validatorAddress, validatorDuties] of dutiesByValidator.entries()) {
174
+ if (validatorDuties.length !== 1) {
175
+ throw new Error(`Validator ${validatorAddress} attested ${validatorDuties.length} times (expected exactly once)`);
176
+ }
177
+ if (!validatorDuties[0].completedAt) {
178
+ throw new Error(`Validator ${validatorAddress} attestation duty not completed`);
179
+ }
180
+ logger?.info(`Validator ${validatorAddress} attested once via node ${validatorDuties[0].nodeId}`);
181
+ }
182
+
183
+ return dutiesByValidator;
184
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './fixtures.js';
2
+ export * from './ha_setup.js';
2
3
  export * from './logging.js';
3
4
  export * from './utils.js';
4
5
  export * from './token_utils.js';
@@ -41,7 +41,7 @@ import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
41
41
  import { SecretValue } from '@aztec/foundation/config';
42
42
  import { randomBytes } from '@aztec/foundation/crypto/random';
43
43
  import { tryRmDir } from '@aztec/foundation/fs';
44
- import { withLogNameSuffix } from '@aztec/foundation/log';
44
+ import { withLoggerBindings } from '@aztec/foundation/log/server';
45
45
  import { retryUntil } from '@aztec/foundation/retry';
46
46
  import { sleep } from '@aztec/foundation/sleep';
47
47
  import { DateProvider, TestDateProvider } from '@aztec/foundation/timer';
@@ -125,14 +125,14 @@ export async function setupSharedBlobStorage(config: { dataDirectory?: string }
125
125
  * @param aztecNode - An instance of Aztec Node.
126
126
  * @param opts - Partial configuration for the PXE.
127
127
  * @param logger - The logger to be used.
128
- * @param useLogSuffix - Whether to add a randomly generated suffix to the PXE debug logs.
128
+ * @param actor - Actor label to include in log output (e.g., 'pxe-test').
129
129
  * @returns A test wallet, logger and teardown function.
130
130
  */
131
131
  export async function setupPXEAndGetWallet(
132
132
  aztecNode: AztecNode,
133
133
  opts: Partial<PXEConfig> = {},
134
134
  logger = getLogger(),
135
- useLogSuffix = false,
135
+ actor?: string,
136
136
  ): Promise<{
137
137
  wallet: TestWallet;
138
138
  logger: Logger;
@@ -150,9 +150,7 @@ export async function setupPXEAndGetWallet(
150
150
 
151
151
  const teardown = configuredDataDirectory ? () => Promise.resolve() : () => tryRmDir(PXEConfig.dataDirectory!);
152
152
 
153
- const wallet = await TestWallet.create(aztecNode, PXEConfig, {
154
- useLogSuffix,
155
- });
153
+ const wallet = await TestWallet.create(aztecNode, PXEConfig, { loggerActorLabel: actor });
156
154
 
157
155
  return {
158
156
  wallet,
@@ -574,10 +572,12 @@ export async function setup(
574
572
  }
575
573
  }
576
574
 
577
- const aztecNodeService = await AztecNodeService.createAndSync(
578
- config,
579
- { dateProvider, telemetry: telemetryClient, p2pClientDeps, logger: createLogger('node:MAIN-aztec-node') },
580
- { prefilledPublicData },
575
+ const aztecNodeService = await withLoggerBindings({ actor: 'node-0' }, () =>
576
+ AztecNodeService.createAndSync(
577
+ config,
578
+ { dateProvider, telemetry: telemetryClient, p2pClientDeps },
579
+ { prefilledPublicData },
580
+ ),
581
581
  );
582
582
  const sequencerClient = aztecNodeService.getSequencer();
583
583
 
@@ -611,7 +611,7 @@ export async function setup(
611
611
  pxeConfig.dataDirectory = path.join(directoryToCleanup, randomBytes(8).toString('hex'));
612
612
  // For tests we only want proving enabled if specifically requested
613
613
  pxeConfig.proverEnabled = !!pxeOpts.proverEnabled;
614
- const wallet = await TestWallet.create(aztecNodeService, pxeConfig);
614
+ const wallet = await TestWallet.create(aztecNodeService, pxeConfig, { loggerActorLabel: 'pxe-0' });
615
615
 
616
616
  if (opts.walletMinFeePadding !== undefined) {
617
617
  wallet.setMinFeePadding(opts.walletMinFeePadding);
@@ -797,7 +797,7 @@ export function createAndSyncProverNode(
797
797
  prefilledPublicData: PublicDataTreeLeaf[] = [],
798
798
  proverNodeDeps: ProverNodeDeps = {},
799
799
  ) {
800
- return withLogNameSuffix('prover-node', async () => {
800
+ return withLoggerBindings({ actor: 'prover-0' }, async () => {
801
801
  const aztecNodeTxProvider = aztecNode && {
802
802
  getTxByHash: aztecNode.getTxByHash.bind(aztecNode),
803
803
  getTxsByHash: aztecNode.getTxsByHash.bind(aztecNode),
@@ -4,14 +4,13 @@
4
4
  import { type AztecNodeConfig, AztecNodeService } from '@aztec/aztec-node';
5
5
  import { range } from '@aztec/foundation/array';
6
6
  import { SecretValue } from '@aztec/foundation/config';
7
- import { addLogNameHandler, removeLogNameHandler } from '@aztec/foundation/log';
7
+ import { withLoggerBindings } from '@aztec/foundation/log/server';
8
8
  import { bufferToHex } from '@aztec/foundation/string';
9
9
  import type { DateProvider } from '@aztec/foundation/timer';
10
10
  import type { ProverNodeConfig, ProverNodeDeps } from '@aztec/prover-node';
11
11
  import type { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
12
12
 
13
13
  import getPort from 'get-port';
14
- import { AsyncLocalStorage } from 'node:async_hooks';
15
14
 
16
15
  import { TEST_PEER_CHECK_INTERVAL_MS } from './fixtures.js';
17
16
  import { createAndSyncProverNode, getPrivateKeyFromIndex } from './utils.js';
@@ -22,6 +21,11 @@ import { getEndToEndTestTelemetryClient } from './with_telemetry_utils.js';
22
21
  // to avoid running validators with the same key
23
22
  export const ATTESTER_PRIVATE_KEYS_START_INDEX = 3;
24
23
 
24
+ // Global counters for actor naming (start at 1)
25
+ let validatorCounter = 1;
26
+ let nodeCounter = 1;
27
+ let proverCounter = 1;
28
+
25
29
  export function generatePrivateKeys(startIndex: number, numberOfKeys: number): `0x${string}`[] {
26
30
  const privateKeys: `0x${string}`[] = [];
27
31
  // Do not start from 0 as it is used during setup
@@ -44,10 +48,6 @@ export async function createNodes(
44
48
  validatorsPerNode = 1,
45
49
  ): Promise<AztecNodeService[]> {
46
50
  const nodePromises: Promise<AztecNodeService>[] = [];
47
- const loggerIdStorage = new AsyncLocalStorage<string>();
48
- const logNameHandler = (module: string) =>
49
- loggerIdStorage.getStore() ? `${module}:${loggerIdStorage.getStore()}` : module;
50
- addLogNameHandler(logNameHandler);
51
51
 
52
52
  for (let i = 0; i < numNodes; i++) {
53
53
  const index = indexOffset + i;
@@ -69,7 +69,6 @@ export async function createNodes(
69
69
  prefilledPublicData,
70
70
  dataDir,
71
71
  metricsPort,
72
- loggerIdStorage,
73
72
  );
74
73
  nodePromises.push(nodePromise);
75
74
  }
@@ -81,7 +80,6 @@ export async function createNodes(
81
80
  throw new Error('Sequencer not found');
82
81
  }
83
82
 
84
- removeLogNameHandler(logNameHandler);
85
83
  return nodes;
86
84
  }
87
85
 
@@ -95,9 +93,9 @@ export async function createNode(
95
93
  prefilledPublicData?: PublicDataTreeLeaf[],
96
94
  dataDirectory?: string,
97
95
  metricsPort?: number,
98
- loggerIdStorage?: AsyncLocalStorage<string>,
99
96
  ) {
100
- const createNode = async () => {
97
+ const actorIndex = validatorCounter++;
98
+ return await withLoggerBindings({ actor: `validator-${actorIndex}` }, async () => {
101
99
  const validatorConfig = await createValidatorConfig(config, bootstrapNode, tcpPort, addressIndex, dataDirectory);
102
100
  const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
103
101
  return await AztecNodeService.createAndSync(
@@ -105,8 +103,7 @@ export async function createNode(
105
103
  { telemetry, dateProvider },
106
104
  { prefilledPublicData, dontStartSequencer: config.dontStartSequencer },
107
105
  );
108
- };
109
- return loggerIdStorage ? await loggerIdStorage.run(tcpPort.toString(), createNode) : createNode();
106
+ });
110
107
  }
111
108
 
112
109
  /** Creates a P2P enabled instance of Aztec Node Service without a validator */
@@ -118,9 +115,9 @@ export async function createNonValidatorNode(
118
115
  prefilledPublicData?: PublicDataTreeLeaf[],
119
116
  dataDirectory?: string,
120
117
  metricsPort?: number,
121
- loggerIdStorage?: AsyncLocalStorage<string>,
122
118
  ) {
123
- const createNode = async () => {
119
+ const actorIndex = nodeCounter++;
120
+ return await withLoggerBindings({ actor: `node-${actorIndex}` }, async () => {
124
121
  const p2pConfig = await createP2PConfig(baseConfig, bootstrapNode, tcpPort, dataDirectory);
125
122
  const config: AztecNodeConfig = {
126
123
  ...p2pConfig,
@@ -130,8 +127,7 @@ export async function createNonValidatorNode(
130
127
  };
131
128
  const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
132
129
  return await AztecNodeService.createAndSync(config, { telemetry, dateProvider }, { prefilledPublicData });
133
- };
134
- return loggerIdStorage ? await loggerIdStorage.run(tcpPort.toString(), createNode) : createNode();
130
+ });
135
131
  }
136
132
 
137
133
  export async function createProverNode(
@@ -143,9 +139,9 @@ export async function createProverNode(
143
139
  prefilledPublicData?: PublicDataTreeLeaf[],
144
140
  dataDirectory?: string,
145
141
  metricsPort?: number,
146
- loggerIdStorage?: AsyncLocalStorage<string>,
147
142
  ) {
148
- const createProverNode = async () => {
143
+ const actorIndex = proverCounter++;
144
+ return await withLoggerBindings({ actor: `prover-${actorIndex}` }, async () => {
149
145
  const proverNodePrivateKey = getPrivateKeyFromIndex(ATTESTER_PRIVATE_KEYS_START_INDEX + addressIndex)!;
150
146
  const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
151
147
 
@@ -165,8 +161,7 @@ export async function createProverNode(
165
161
  prefilledPublicData,
166
162
  { ...proverNodeDeps, telemetry },
167
163
  );
168
- };
169
- return loggerIdStorage ? await loggerIdStorage.run(tcpPort.toString(), createProverNode) : createProverNode();
164
+ });
170
165
  }
171
166
 
172
167
  export async function createP2PConfig(
@@ -11,7 +11,7 @@ import type { DeployAztecL1ContractsReturnType } from '@aztec/ethereum/deploy-az
11
11
  import { deployL1Contract } from '@aztec/ethereum/deploy-l1-contract';
12
12
  import type { ExtendedViemWalletClient } from '@aztec/ethereum/types';
13
13
  import { extractEvent } from '@aztec/ethereum/utils';
14
- import { CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
14
+ import { EpochNumber } from '@aztec/foundation/branded-types';
15
15
  import { sha256ToField } from '@aztec/foundation/crypto/sha256';
16
16
  import { InboxAbi, UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts';
17
17
  import { UniswapContract } from '@aztec/noir-contracts.js/Uniswap';
@@ -250,8 +250,8 @@ export const uniswapL1L2TestSuite = (
250
250
  await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n);
251
251
 
252
252
  // Since the outbox is only consumable when the epoch is proven, we need to advance to the next epoch.
253
- const checkpointNumber = CheckpointNumber.fromBlockNumber(l2UniswapInteractionReceipt.blockNumber!);
254
- const epoch = await rollup.getEpochNumberForCheckpoint(checkpointNumber);
253
+ const block = await aztecNode.getBlock(l2UniswapInteractionReceipt.blockNumber!);
254
+ const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber);
255
255
  await cheatCodes.rollup.advanceToEpoch(EpochNumber(epoch + 1));
256
256
  await waitForProven(aztecNode, l2UniswapInteractionReceipt, { provenTimeout: 300 });
257
257
 
@@ -838,9 +838,8 @@ export const uniswapL1L2TestSuite = (
838
838
  chainId: new Fr(l1Client.chain.id),
839
839
  });
840
840
 
841
- const epoch = await rollup.getEpochNumberForCheckpoint(
842
- CheckpointNumber.fromBlockNumber(withdrawReceipt.blockNumber!),
843
- );
841
+ const block = await aztecNode.getBlock(withdrawReceipt.blockNumber!);
842
+ const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber);
844
843
  const swapResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, swapPrivateLeaf);
845
844
  const withdrawResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, withdrawLeaf);
846
845
 
@@ -972,9 +971,8 @@ export const uniswapL1L2TestSuite = (
972
971
  chainId: new Fr(l1Client.chain.id),
973
972
  });
974
973
 
975
- const epoch = await rollup.getEpochNumberForCheckpoint(
976
- CheckpointNumber.fromBlockNumber(withdrawReceipt.blockNumber!),
977
- );
974
+ const block = await aztecNode.getBlock(withdrawReceipt.blockNumber!);
975
+ const epoch = await rollup.getEpochNumberForCheckpoint(block!.checkpointNumber);
978
976
  const swapResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, swapPublicLeaf);
979
977
  const withdrawResult = await computeL2ToL1MembershipWitness(aztecNode, epoch, withdrawLeaf);
980
978
 
@@ -15,6 +15,7 @@ const testConfigSchema = z.object({
15
15
  AZTEC_EPOCH_DURATION: z.coerce.number().optional().default(32),
16
16
  AZTEC_PROOF_SUBMISSION_WINDOW: z.coerce.number().optional().default(5),
17
17
  AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET: z.coerce.number().optional().default(2),
18
+ FUNDING_PRIVATE_KEY: z.string().optional(),
18
19
  });
19
20
 
20
21
  export type TestConfig = z.infer<typeof testConfigSchema>;
@@ -5,7 +5,7 @@
5
5
  export { type TestConfig, setupEnvironment } from './config.js';
6
6
 
7
7
  // Scripts
8
- export { getGitProjectRoot, getAztecBin, runAztecBin, runProjectScript } from './scripts.js';
8
+ export { type ScriptResult, getGitProjectRoot, getAztecBin, runAztecBin, runProjectScript } from './scripts.js';
9
9
 
10
10
  // K8s operations
11
11
  export {
@@ -24,6 +24,7 @@ export {
24
24
  getServiceEndpoint,
25
25
  getRPCEndpoint,
26
26
  getEthereumEndpoint,
27
+ createResilientPrometheusConnection,
27
28
  } from './k8s.js';
28
29
 
29
30
  // Chaos Mesh
@@ -45,6 +46,7 @@ export { restartBot, installTransferBot, uninstallTransferBot } from './bot.js';
45
46
  // Node operations (sequencers, validators, pods)
46
47
  export {
47
48
  awaitCheckpointNumber,
49
+ waitForProvenToAdvance,
48
50
  getSequencers,
49
51
  updateSequencersConfig,
50
52
  getSequencersConfig,
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from '@aztec/aztec.js/log';
2
+ import type { Logger } from '@aztec/foundation/log';
2
3
  import { promiseWithResolvers } from '@aztec/foundation/promise';
3
4
  import { retryUntil } from '@aztec/foundation/retry';
4
5
 
@@ -6,6 +7,8 @@ import { type ChildProcess, exec, spawn } from 'child_process';
6
7
  import path from 'path';
7
8
  import { promisify } from 'util';
8
9
 
10
+ import { AlertTriggeredError, GrafanaClient } from '../../quality_of_service/grafana_client.js';
11
+
9
12
  const execAsync = promisify(exec);
10
13
 
11
14
  const logger = createLogger('e2e:k8s-utils');
@@ -370,6 +373,155 @@ export async function waitForResourcesByName({
370
373
  );
371
374
  }
372
375
 
376
+ /**
377
+ * Waits for all StatefulSets matching a label to have all their replicas ready.
378
+ *
379
+ * @param namespace - Kubernetes namespace
380
+ * @param label - Label selector for StatefulSets (e.g., "app.kubernetes.io/component=sequencer-node")
381
+ * @param timeoutSeconds - Maximum time to wait in seconds
382
+ * @param pollIntervalSeconds - How often to check status
383
+ */
384
+ export async function waitForStatefulSetsReady({
385
+ namespace,
386
+ label,
387
+ timeoutSeconds = 600,
388
+ pollIntervalSeconds = 5,
389
+ }: {
390
+ namespace: string;
391
+ label: string;
392
+ timeoutSeconds?: number;
393
+ pollIntervalSeconds?: number;
394
+ }): Promise<void> {
395
+ logger.info(`Waiting for StatefulSets with label ${label} to have all replicas ready (timeout: ${timeoutSeconds}s)`);
396
+
397
+ await retryUntil(
398
+ async () => {
399
+ // Get all StatefulSets matching the label
400
+ const getCmd = `kubectl get statefulset -l ${label} -n ${namespace} -o json`;
401
+ const { stdout } = await execAsync(getCmd);
402
+ const result = JSON.parse(stdout);
403
+
404
+ if (!result.items || result.items.length === 0) {
405
+ logger.verbose(`No StatefulSets found with label ${label}`);
406
+ return false;
407
+ }
408
+
409
+ // Check each StatefulSet
410
+ for (const sts of result.items) {
411
+ const name = sts.metadata.name;
412
+ const desired = sts.spec.replicas ?? 0;
413
+ const ready = sts.status.readyReplicas ?? 0;
414
+ const updated = sts.status.updatedReplicas ?? 0;
415
+
416
+ if (ready < desired || updated < desired) {
417
+ logger.verbose(`StatefulSet ${name}: ${ready}/${desired} ready, ${updated}/${desired} updated`);
418
+ return false;
419
+ }
420
+ }
421
+
422
+ logger.info(`All StatefulSets with label ${label} are ready`);
423
+ return true;
424
+ },
425
+ `StatefulSets with label ${label} to be ready`,
426
+ timeoutSeconds,
427
+ pollIntervalSeconds,
428
+ );
429
+ }
430
+
431
+ /**
432
+ * Creates a Prometheus connection that can re-establish port-forward on failure.
433
+ * Returns functions to connect and run alert checks that automatically reconnect if needed.
434
+ *
435
+ * @param namespace - K8s namespace to fall back to if metrics namespace doesn't have Prometheus
436
+ * @param endpoints - Array to track created endpoints for cleanup
437
+ * @param log - Logger instance
438
+ */
439
+ export function createResilientPrometheusConnection(
440
+ namespace: string,
441
+ endpoints: ServiceEndpoint[],
442
+ log: Logger,
443
+ ): {
444
+ connect: () => Promise<GrafanaClient>;
445
+ runAlertCheck: (alerts: Parameters<GrafanaClient['runAlertCheck']>[0]) => Promise<void>;
446
+ } {
447
+ let alertChecker: GrafanaClient | undefined;
448
+ let currentEndpoint: ServiceEndpoint | undefined;
449
+
450
+ const connect = async (): Promise<GrafanaClient> => {
451
+ // Kill existing connection if any
452
+ if (currentEndpoint?.process) {
453
+ currentEndpoint.process.kill();
454
+ }
455
+
456
+ // Try metrics namespace first, then network namespace
457
+ let promPort = 0;
458
+ let promUrl = '';
459
+ let promProc: ChildProcess | undefined;
460
+
461
+ try {
462
+ const metricsResult = await startPortForward({
463
+ resource: `svc/metrics-prometheus-server`,
464
+ namespace: 'metrics',
465
+ containerPort: 80,
466
+ });
467
+ promProc = metricsResult.process;
468
+ promPort = metricsResult.port;
469
+ promUrl = `http://127.0.0.1:${promPort}/api/v1`;
470
+ } catch {
471
+ // Metrics namespace might not have Prometheus, try network namespace
472
+ log.verbose('Metrics namespace Prometheus not available, trying network namespace');
473
+ }
474
+
475
+ if (promPort === 0) {
476
+ const nsResult = await startPortForward({
477
+ resource: `svc/prometheus-server`,
478
+ namespace,
479
+ containerPort: 80,
480
+ });
481
+ promProc = nsResult.process;
482
+ promPort = nsResult.port;
483
+ promUrl = `http://127.0.0.1:${promPort}/api/v1`;
484
+ }
485
+
486
+ if (!promProc || promPort === 0) {
487
+ throw new Error('Unable to port-forward to Prometheus');
488
+ }
489
+
490
+ currentEndpoint = { url: promUrl, process: promProc };
491
+ endpoints.push(currentEndpoint);
492
+ alertChecker = new GrafanaClient(log, { grafanaEndpoint: promUrl, grafanaCredentials: '' });
493
+ log.info(`Established Prometheus connection at ${promUrl}`);
494
+ return alertChecker;
495
+ };
496
+
497
+ const runAlertCheck = async (alerts: Parameters<GrafanaClient['runAlertCheck']>[0]): Promise<void> => {
498
+ if (!alertChecker) {
499
+ alertChecker = await connect();
500
+ }
501
+
502
+ try {
503
+ await alertChecker.runAlertCheck(alerts);
504
+ } catch (err) {
505
+ // If it's an AlertTriggeredError (expected behavior)
506
+ if (err instanceof AlertTriggeredError) {
507
+ throw err;
508
+ }
509
+
510
+ // Check if it's a connection error (port-forward died)
511
+ const errorStr = String(err);
512
+ if (errorStr.includes('fetch failed') || errorStr.includes('ECONNREFUSED') || errorStr.includes('ECONNRESET')) {
513
+ log.warn(`Prometheus connection lost, re-establishing port-forward...`);
514
+ alertChecker = await connect();
515
+ await alertChecker.runAlertCheck(alerts);
516
+ } else {
517
+ throw err;
518
+ }
519
+ }
520
+ };
521
+
522
+ return { connect, runAlertCheck };
523
+ }
524
+
373
525
  export function getChartDir(spartanDir: string, chartName: string) {
374
526
  return path.join(spartanDir.trim(), chartName);
375
527
  }