@aztec/ethereum 0.0.1-commit.993d240 → 0.0.1-commit.9a89641

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 (105) hide show
  1. package/dest/client.d.ts +11 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +35 -3
  4. package/dest/config.d.ts +22 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +54 -3
  7. package/dest/contracts/governance.d.ts +36 -11
  8. package/dest/contracts/governance.d.ts.map +1 -1
  9. package/dest/contracts/governance.js +54 -24
  10. package/dest/contracts/governance_proposer.d.ts +7 -8
  11. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  12. package/dest/contracts/governance_proposer.js +7 -10
  13. package/dest/contracts/gse.d.ts +8 -2
  14. package/dest/contracts/gse.d.ts.map +1 -1
  15. package/dest/contracts/gse.js +11 -2
  16. package/dest/contracts/multicall.d.ts +5 -3
  17. package/dest/contracts/multicall.d.ts.map +1 -1
  18. package/dest/contracts/multicall.js +7 -5
  19. package/dest/contracts/outbox.d.ts +8 -2
  20. package/dest/contracts/outbox.d.ts.map +1 -1
  21. package/dest/contracts/outbox.js +6 -2
  22. package/dest/contracts/rollup.d.ts +176 -9
  23. package/dest/contracts/rollup.d.ts.map +1 -1
  24. package/dest/contracts/rollup.js +48 -8
  25. package/dest/contracts/slashing_proposer.d.ts +37 -6
  26. package/dest/contracts/slashing_proposer.d.ts.map +1 -1
  27. package/dest/contracts/slashing_proposer.js +447 -17
  28. package/dest/deploy_aztec_l1_contracts.d.ts +16 -5
  29. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  30. package/dest/deploy_aztec_l1_contracts.js +26 -13
  31. package/dest/foundry_binary.d.ts +13 -0
  32. package/dest/foundry_binary.d.ts.map +1 -0
  33. package/dest/foundry_binary.js +52 -0
  34. package/dest/l1_artifacts.d.ts +1242 -95
  35. package/dest/l1_artifacts.d.ts.map +1 -1
  36. package/dest/l1_tx_utils/config.js +3 -3
  37. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
  38. package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
  39. package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
  40. package/dest/l1_tx_utils/l1_tx_utils.d.ts +1 -1
  41. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  42. package/dest/l1_tx_utils/l1_tx_utils.js +84 -22
  43. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  44. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  45. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
  46. package/dest/l1_tx_utils/types.d.ts +27 -1
  47. package/dest/l1_tx_utils/types.d.ts.map +1 -1
  48. package/dest/l1_tx_utils/types.js +10 -0
  49. package/dest/publisher_manager.d.ts +18 -6
  50. package/dest/publisher_manager.d.ts.map +1 -1
  51. package/dest/publisher_manager.js +44 -7
  52. package/dest/queries.d.ts +1 -1
  53. package/dest/queries.d.ts.map +1 -1
  54. package/dest/queries.js +7 -1
  55. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  56. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  57. package/dest/test/blob_kzg_warmup.js +64 -0
  58. package/dest/test/chain_monitor.d.ts +33 -2
  59. package/dest/test/chain_monitor.d.ts.map +1 -1
  60. package/dest/test/chain_monitor.js +71 -6
  61. package/dest/test/eth_cheat_codes.d.ts +26 -3
  62. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  63. package/dest/test/eth_cheat_codes.js +72 -37
  64. package/dest/test/index.d.ts +2 -1
  65. package/dest/test/index.d.ts.map +1 -1
  66. package/dest/test/index.js +1 -0
  67. package/dest/test/rollup_cheat_codes.d.ts +30 -1
  68. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  69. package/dest/test/rollup_cheat_codes.js +27 -0
  70. package/dest/test/start_anvil.d.ts +1 -1
  71. package/dest/test/start_anvil.d.ts.map +1 -1
  72. package/dest/test/start_anvil.js +41 -5
  73. package/dest/utils.d.ts +2 -2
  74. package/dest/utils.d.ts.map +1 -1
  75. package/dest/utils.js +15 -6
  76. package/package.json +5 -6
  77. package/src/client.ts +39 -1
  78. package/src/config.ts +73 -3
  79. package/src/contracts/governance.ts +63 -20
  80. package/src/contracts/governance_proposer.ts +11 -15
  81. package/src/contracts/gse.ts +19 -2
  82. package/src/contracts/multicall.ts +8 -5
  83. package/src/contracts/outbox.ts +6 -2
  84. package/src/contracts/rollup.ts +57 -8
  85. package/src/contracts/slashing_proposer.ts +78 -15
  86. package/src/deploy_aztec_l1_contracts.ts +31 -13
  87. package/src/foundry_binary.ts +57 -0
  88. package/src/l1_tx_utils/config.ts +3 -3
  89. package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
  90. package/src/l1_tx_utils/l1_tx_utils.ts +71 -20
  91. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +2 -1
  92. package/src/l1_tx_utils/types.ts +32 -0
  93. package/src/publisher_manager.ts +54 -18
  94. package/src/queries.ts +6 -0
  95. package/src/test/blob_kzg_warmup.ts +65 -0
  96. package/src/test/chain_monitor.ts +88 -5
  97. package/src/test/eth_cheat_codes.ts +66 -41
  98. package/src/test/index.ts +1 -0
  99. package/src/test/rollup_cheat_codes.ts +51 -0
  100. package/src/test/start_anvil.ts +38 -6
  101. package/src/utils.ts +12 -6
  102. package/dest/generated/l1-contracts-defaults.d.ts +0 -30
  103. package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
  104. package/dest/generated/l1-contracts-defaults.js +0 -30
  105. package/src/generated/l1-contracts-defaults.ts +0 -32
@@ -14,7 +14,7 @@ import {
14
14
  multicall3Abi,
15
15
  } from 'viem';
16
16
 
17
- import type { L1BlobInputs, L1TxConfig, L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
17
+ import type { L1BlobInputs, L1TxConfig, L1TxRequest, L1TxState, L1TxUtils } from '../l1_tx_utils/index.js';
18
18
  import type { ExtendedViemWalletClient } from '../types.js';
19
19
  import { tryDecodeRevertReason } from '../utils.js';
20
20
 
@@ -26,7 +26,10 @@ export const MULTI_CALL_3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11'
26
26
  * treat it as a fatal on-chain failure rather than retrying on a different publisher.
27
27
  */
28
28
  export class MulticallForwarderRevertedError extends Error {
29
- constructor(public readonly receipt: TransactionReceipt) {
29
+ constructor(
30
+ public readonly receipt: TransactionReceipt,
31
+ public readonly txState?: L1TxState,
32
+ ) {
30
33
  super(`Multicall3 forwarder tx reverted: ${receipt.transactionHash}`);
31
34
  this.name = 'MulticallForwarderRevertedError';
32
35
  }
@@ -209,7 +212,7 @@ export class Multicall3 {
209
212
  args: [args],
210
213
  });
211
214
 
212
- const { receipt } = await l1TxUtils.sendAndMonitorTransaction(
215
+ const { receipt, state } = await l1TxUtils.sendAndMonitorTransaction(
213
216
  {
214
217
  to: MULTI_CALL_3_ADDRESS,
215
218
  data: encodedForwarderData,
@@ -223,11 +226,11 @@ export class Multicall3 {
223
226
  // allowFailure to true for all calls, so a reverted status here would indicate a problem with
224
227
  // the Multicall3 contract itself or the forwarder transaction (such as an out-of-gas).
225
228
  if (receipt.status !== 'success') {
226
- throw new MulticallForwarderRevertedError(receipt);
229
+ throw new MulticallForwarderRevertedError(receipt, state);
227
230
  }
228
231
 
229
232
  const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
230
- return { receipt, stats, multicallData: encodedForwarderData };
233
+ return { receipt, stats, multicallData: encodedForwarderData, state };
231
234
  }
232
235
 
233
236
  /** Batch multiple value transfers into a single aggregate3Value call on Multicall3. */
@@ -89,9 +89,13 @@ export class OutboxContract {
89
89
  * Returns every root stored for `epoch`. Slot `i` of the returned array holds the root inserted
90
90
  * for `numCheckpointsInEpoch = i + 1`, or `Fr.ZERO` if no proof of that depth has been inserted
91
91
  * yet. The array length is always `MAX_CHECKPOINTS_PER_EPOCH`.
92
+ *
93
+ * @param opts - Optional viem read options. Pass `blockNumber` to pin the read to a specific L1
94
+ * block, which is important for callers that want a consistent snapshot across multiple reads
95
+ * (e.g. the archiver's outbox-trees resolver, which pins to the node's synced L1 block).
92
96
  */
93
- public async getRoots(epoch: EpochNumber): Promise<Fr[]> {
94
- const raw = await this.outbox.read.getRoots([BigInt(epoch)]);
97
+ public async getRoots(epoch: EpochNumber, opts?: { blockNumber?: bigint }): Promise<Fr[]> {
98
+ const raw = await this.outbox.read.getRoots([BigInt(epoch)], opts);
95
99
  return raw.map(hex => Fr.fromString(hex));
96
100
  }
97
101
 
@@ -6,6 +6,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import type { ViemSignature } from '@aztec/foundation/eth-signature';
7
7
  import { createLogger } from '@aztec/foundation/log';
8
8
  import { makeBackoff, retry } from '@aztec/foundation/retry';
9
+ import { getErrorCause } from '@aztec/foundation/types';
9
10
  import { EscapeHatchAbi } from '@aztec/l1-artifacts/EscapeHatchAbi';
10
11
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
11
12
  import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
@@ -13,11 +14,14 @@ import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
13
14
  import chunk from 'lodash.chunk';
14
15
  import {
15
16
  type Account,
17
+ ContractFunctionRevertedError,
16
18
  type GetContractReturnType,
17
19
  type Hex,
18
20
  type Log,
21
+ RpcRequestError,
19
22
  type StateOverride,
20
23
  type WatchContractEventReturnType,
24
+ decodeErrorResult,
21
25
  encodeAbiParameters,
22
26
  encodeFunctionData,
23
27
  getContract,
@@ -79,6 +83,7 @@ export type ViemHeader = {
79
83
  feeRecipient: `0x${string}`;
80
84
  gasFees: ViemGasFees;
81
85
  totalManaUsed: bigint;
86
+ accumulatedFees: bigint;
82
87
  };
83
88
 
84
89
  export type ViemGasFees = {
@@ -230,6 +235,32 @@ export type CheckpointProposedArgs = {
230
235
  /** Log type for CheckpointProposed events. */
231
236
  export type CheckpointProposedLog = L1EventLog<CheckpointProposedArgs>;
232
237
 
238
+ const INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR = 'ValidatorSelection__InsufficientValidatorSetSize';
239
+
240
+ function isValidatorSelectionError(err: unknown, errorName: string): boolean {
241
+ return (
242
+ getErrorCause(err, ContractFunctionRevertedError)?.data?.errorName === errorName ||
243
+ decodeRpcRequestErrorName(err) === errorName
244
+ );
245
+ }
246
+
247
+ function decodeRpcRequestErrorName(err: unknown): string | undefined {
248
+ const data = getErrorCause(err, RpcRequestError)?.data;
249
+ if (!isHexString(data)) {
250
+ return undefined;
251
+ }
252
+
253
+ try {
254
+ return decodeErrorResult({ abi: RollupAbi, data }).errorName;
255
+ } catch {
256
+ return undefined;
257
+ }
258
+ }
259
+
260
+ function isHexString(value: unknown): value is Hex {
261
+ return typeof value === 'string' && value.startsWith('0x');
262
+ }
263
+
233
264
  export class RollupContract {
234
265
  private readonly rollup: GetContractReturnType<typeof RollupAbi, ViemClient>;
235
266
  private readonly logger = createLogger('ethereum:rollup');
@@ -524,8 +555,19 @@ export class RollupContract {
524
555
  return EthAddress.fromString(await this.rollup.read.owner());
525
556
  }
526
557
 
527
- async getActiveAttesterCount(): Promise<number> {
528
- return Number(await this.rollup.read.getActiveAttesterCount());
558
+ async getActiveAttesterCount(options?: { blockNumber?: bigint }): Promise<number> {
559
+ await checkBlockTag(options?.blockNumber, this.client);
560
+ return Number(await this.rollup.read.getActiveAttesterCount(options));
561
+ }
562
+
563
+ /**
564
+ * Number of attesters that were staked at the given (historical) L1 timestamp. This is the count that
565
+ * validator-set sampling uses to decide whether an epoch gets a committee, and the historical counterpart
566
+ * of {@link getActiveAttesterCount} (which reads at the latest L1 block).
567
+ */
568
+ async getAttesterCountAtTime(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<number> {
569
+ const gse = new GSEContract(this.client, await this.getGSE());
570
+ return gse.getAttesterCountAtTime(this.address, timestamp, options);
529
571
  }
530
572
 
531
573
  public async getSlashingProposerAddress() {
@@ -588,7 +630,7 @@ export class RollupContract {
588
630
  args: [timestamp],
589
631
  })
590
632
  .catch(e => {
591
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
633
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
592
634
  return { result: undefined };
593
635
  }
594
636
  throw e;
@@ -618,7 +660,7 @@ export class RollupContract {
618
660
  args: [],
619
661
  })
620
662
  .catch(e => {
621
- if (e instanceof Error && e.message.includes('ValidatorSelection__InsufficientValidatorSetSize')) {
663
+ if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
622
664
  return { result: undefined };
623
665
  }
624
666
  throw e;
@@ -782,7 +824,7 @@ export class RollupContract {
782
824
  }
783
825
 
784
826
  async getEpochProofPublicInputs(
785
- args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly `0x${string}`[], `0x${string}`],
827
+ args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly ViemHeader[], `0x${string}`],
786
828
  ): Promise<Fr[]> {
787
829
  const result = await this.rollup.read.getEpochProofPublicInputs(args);
788
830
  return result.map(Fr.fromString);
@@ -1160,13 +1202,20 @@ export class RollupContract {
1160
1202
  }
1161
1203
 
1162
1204
  async getAttesters(timestamp?: bigint): Promise<EthAddress[]> {
1163
- const attesterSize = await this.getActiveAttesterCount();
1205
+ // Pin every read to a single L1 block so the attester count and the chunked index reads
1206
+ // observe a consistent set. Without this, the count and each chunk default to `latest` and
1207
+ // can straddle a block boundary (or reorg), yielding an inconsistent or truncated set.
1208
+ const block = await this.client.getBlock();
1209
+ const blockNumber = block.number ?? undefined;
1210
+ const ts = timestamp ?? block.timestamp;
1211
+ const attesterSize = await this.getActiveAttesterCount({ blockNumber });
1164
1212
  const gse = new GSEContract(this.client, await this.getGSE());
1165
- const ts = timestamp ?? (await this.client.getBlock()).timestamp;
1166
1213
  const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
1167
1214
  const chunks = chunk(indices, 1000);
1168
1215
 
1169
- const results = await Promise.all(chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk)));
1216
+ const results = await Promise.all(
1217
+ chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk, { blockNumber })),
1218
+ );
1170
1219
  return results.flat().map(addr => EthAddress.fromString(addr));
1171
1220
  }
1172
1221
 
@@ -3,6 +3,7 @@ import type { ViemClient } from '@aztec/ethereum/types';
3
3
  import { mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
4
4
  import { SlotNumber } from '@aztec/foundation/branded-types';
5
5
  import { Buffer32 } from '@aztec/foundation/buffer';
6
+ import { memoize } from '@aztec/foundation/decorators';
6
7
  import { EthAddress } from '@aztec/foundation/eth-address';
7
8
  import { Signature } from '@aztec/foundation/eth-signature';
8
9
  import { hexToBuffer } from '@aztec/foundation/string';
@@ -25,6 +26,14 @@ import {
25
26
  export class SlashingProposerContract {
26
27
  private readonly contract: GetContractReturnType<typeof SlashingProposerAbi, ViemClient>;
27
28
 
29
+ /**
30
+ * Slash target validators of the last round asked for. Safe to cache because a round's targets are the committees
31
+ * of epochs that had already ended when the round opened (see SLASH_OFFSET_IN_ROUNDS), and those committees are
32
+ * sampled from validator set and randao snapshots taken before the epoch started, so they cannot change while the
33
+ * round is live. The promise is cached rather than the result, so concurrent callers share one in-flight read.
34
+ */
35
+ private slashTargetValidators: { round: bigint; validators: Promise<EthAddress[]> } | undefined;
36
+
28
37
  constructor(
29
38
  public readonly client: ViemClient,
30
39
  address: Hex | EthAddress,
@@ -64,6 +73,8 @@ export class SlashingProposerContract {
64
73
  return this.contract.read.EXECUTION_DELAY_IN_ROUNDS();
65
74
  }
66
75
 
76
+ /** Returns the slash amounts for the three slash unit levels. Immutable on the contract, so memoized. */
77
+ @memoize
67
78
  public getSlashingAmounts(): Promise<[bigint, bigint, bigint]> {
68
79
  return Promise.all([
69
80
  this.contract.read.SLASH_AMOUNT_SMALL(),
@@ -234,35 +245,70 @@ export class SlashingProposerContract {
234
245
  };
235
246
  }
236
247
 
248
+ /**
249
+ * Returns the validators eligible to be voted against in a round, in the order votes encode them. Cached for the
250
+ * last round asked for: reading them runs a committee sampling simulation on L1, and every vote of a round decodes
251
+ * against the same list, so a caller walking a round's votes would otherwise repeat that call per vote. Only the
252
+ * last round is kept since callers move forward round by round.
253
+ */
254
+ public getSlashTargetValidators(round: bigint): Promise<EthAddress[]> {
255
+ const cached = this.slashTargetValidators;
256
+ if (cached?.round === round) {
257
+ return cached.validators;
258
+ }
259
+
260
+ const entry = { round, validators: this.fetchSlashTargetValidators(round) };
261
+ this.slashTargetValidators = entry;
262
+ // A failed read must not stay cached, or every later vote of the round would replay the same rejection
263
+ entry.validators.catch(() => {
264
+ if (this.slashTargetValidators === entry) {
265
+ this.slashTargetValidators = undefined;
266
+ }
267
+ });
268
+ return entry.validators;
269
+ }
270
+
271
+ private async fetchSlashTargetValidators(round: bigint): Promise<EthAddress[]> {
272
+ const { result } = await this.contract.simulate.getSlashTargetCommittees([round]);
273
+ return result.flat().map(validator => EthAddress.fromString(validator));
274
+ }
275
+
276
+ /**
277
+ * Returns the slash amount voted for each target validator by a single vote of a round.
278
+ * @param index - Position of the vote within the round, from 0 (inclusive) to the round's vote count (exclusive)
279
+ */
280
+ public async getVoteAt(round: bigint, index: bigint): Promise<SlashVoteTarget[]> {
281
+ const [validators, vote, slashAmounts] = await Promise.all([
282
+ this.getSlashTargetValidators(round),
283
+ this.contract.read.getVotes([round, index]),
284
+ this.getSlashingAmounts(),
285
+ ]);
286
+ return decodeVote(vote, validators, slashAmounts);
287
+ }
288
+
237
289
  /** Returns the last vote emitted for a given round */
238
290
  public async getLastVote(round: bigint) {
239
291
  const { voteCount } = await this.getRound(round);
240
- const validators = (await this.contract.simulate.getSlashTargetCommittees([round])).result.flat();
241
- const vote = await this.contract.read.getVotes([round, voteCount - 1n]);
242
- const decoded = decodeSlashConsensusVotes(hexToBuffer(vote));
243
- const slashAmounts = await this.getSlashingAmounts();
244
- return decoded
245
- .map((units, i) => ({
246
- validator: EthAddress.fromString(validators[i]),
247
- slashAmount: slashAmounts[units - 1] ?? 0n,
248
- }))
249
- .filter(v => v.slashAmount > 0n);
292
+ return await this.getVoteAt(round, voteCount - 1n);
250
293
  }
251
294
 
252
295
  /**
253
296
  * Listen for VoteCast events
254
- * @param callback - Callback function to handle vote cast events
297
+ * @param callback - Callback receiving the round, the vote's index within the round as accepted by `getVoteAt`,
298
+ * and the proposer that cast it
255
299
  * @returns Unwatch function
256
300
  */
257
- public listenToVoteCast(callback: (args: { round: bigint; proposer: string }) => void): () => void {
301
+ public listenToVoteCast(
302
+ callback: (args: { round: bigint; voteIndex: bigint; proposer: string }) => void,
303
+ ): () => void {
258
304
  return this.contract.watchEvent.VoteCast(
259
305
  {},
260
306
  {
261
307
  onLogs: logs => {
262
308
  for (const log of logs) {
263
- const { round, proposer } = log.args;
264
- if (round !== undefined && proposer) {
265
- callback({ round, proposer });
309
+ const { round, voteIndex, proposer } = log.args;
310
+ if (round !== undefined && voteIndex !== undefined && proposer) {
311
+ callback({ round, voteIndex, proposer });
266
312
  }
267
313
  }
268
314
  },
@@ -294,6 +340,23 @@ export class SlashingProposerContract {
294
340
  }
295
341
  }
296
342
 
343
+ /**
344
+ * A validator targeted by a slashing vote, with the amount voted. The position is the validator's index in the
345
+ * round's flattened slash target committees — the unit the contract tallies quorum by. A validator sitting in
346
+ * several of the round's committees holds several positions, each with its own tally.
347
+ */
348
+ export type SlashVoteTarget = { validator: EthAddress; slashAmount: bigint; position: number };
349
+
350
+ function decodeVote(vote: Hex, validators: EthAddress[], slashAmounts: [bigint, bigint, bigint]): SlashVoteTarget[] {
351
+ return decodeSlashConsensusVotes(hexToBuffer(vote))
352
+ .map((units, position) => ({
353
+ validator: validators[position],
354
+ slashAmount: slashAmounts[units - 1] ?? 0n,
355
+ position,
356
+ }))
357
+ .filter(v => v.slashAmount > 0n);
358
+ }
359
+
297
360
  /**
298
361
  * Decodes a Buffer containing slash votes back into an array of numbers.
299
362
  * Each vote is represented as a 2-bit value (0, 1, 2, or 3) representing slashing units.
@@ -5,12 +5,12 @@ import { jsonStringify } from '@aztec/foundation/json-rpc';
5
5
  import { createLogger } from '@aztec/foundation/log';
6
6
  import { promiseWithResolvers } from '@aztec/foundation/promise';
7
7
  import type { Fr } from '@aztec/foundation/schemas';
8
- import { fileURLToPath } from '@aztec/foundation/url';
9
8
 
10
9
  import { bn254 } from '@noble/curves/bn254';
11
10
  import type { Abi, Narrow } from 'abitype';
12
11
  import { spawn } from 'child_process';
13
12
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
13
+ import { createRequire } from 'node:module';
14
14
  import { tmpdir } from 'os';
15
15
  import { dirname, join, resolve } from 'path';
16
16
  import readline from 'readline';
@@ -19,14 +19,17 @@ import { mainnet, sepolia } from 'viem/chains';
19
19
 
20
20
  import { createEthereumChain, isAnvilTestChain } from './chain.js';
21
21
  import { createExtendedL1Client } from './client.js';
22
- import type { L1ContractsConfig } from './config.js';
22
+ import { type L1ContractsConfig, assertValidSlotDurations } from './config.js';
23
23
  import { deployMulticall3 } from './contracts/multicall.js';
24
24
  import { RollupContract } from './contracts/rollup.js';
25
+ import { resolveFoundryBinary } from './foundry_binary.js';
25
26
  import type { L1ContractAddresses } from './l1_contract_addresses.js';
26
27
  import type { ExtendedViemWalletClient } from './types.js';
27
28
 
28
29
  const logger = createLogger('ethereum:deploy_aztec_l1_contracts');
29
30
 
31
+ const require = createRequire(import.meta.url);
32
+
30
33
  const JSON_DEPLOY_RESULT_PREFIX = 'JSON DEPLOY RESULT:';
31
34
 
32
35
  /**
@@ -93,11 +96,16 @@ function runProcess<T>(
93
96
 
94
97
  // Covers an edge where where we may have a cached BlobLib that is not meant for production.
95
98
  // Despite the profile apparently sometimes cached code remains (so says Lasse after his ignition-monorepo arc).
96
- async function maybeForgeForceProductionBuild(l1ContractsPath: string, script: string, chainId: number) {
99
+ async function maybeForgeForceProductionBuild(
100
+ forgeBin: string,
101
+ l1ContractsPath: string,
102
+ script: string,
103
+ chainId: number,
104
+ ) {
97
105
  if (chainId === mainnet.id) {
98
106
  logger.info(`Recompiling ${script} with production profile for mainnet deployment`);
99
107
  logger.info('This may take a minute but ensures production BlobLib is used.');
100
- await runProcess('forge', ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
108
+ await runProcess(forgeBin, ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
101
109
  }
102
110
  }
103
111
 
@@ -123,15 +131,13 @@ export interface ValidatorJson {
123
131
  }
124
132
 
125
133
  /**
126
- * Gets the path to the l1-contracts foundry artifacts directory.
127
- * These are copied from l1-contracts to yarn-project/l1-artifacts/l1-contracts
128
- * during build to make yarn-project self-contained.
134
+ * Gets the path to the l1-contracts foundry artifacts directory bundled inside @aztec/l1-artifacts.
135
+ * Resolved through the package (its "." export -> dest/index.js) so it works whether the package is
136
+ * linked via portal (monorepo) or installed under node_modules (published npm) — resolution follows
137
+ * the symlink in the portal case. The bundled foundry subtree sits alongside dest/, at <pkg>/l1-contracts.
129
138
  */
130
139
  export function getL1ContractsPath(): string {
131
- const currentDir = dirname(fileURLToPath(import.meta.url));
132
- // Go up from yarn-project/ethereum/dest to yarn-project, then to l1-artifacts/l1-contracts
133
- const l1ContractsPath = resolve(currentDir, '..', '..', 'l1-artifacts', 'l1-contracts');
134
- return l1ContractsPath;
140
+ return resolve(dirname(require.resolve('@aztec/l1-artifacts')), '..', 'l1-contracts');
135
141
  }
136
142
 
137
143
  // Cached deployment directory
@@ -282,6 +288,7 @@ export async function deployAztecL1Contracts(
282
288
  args: DeployAztecL1ContractsArgs,
283
289
  ): Promise<DeployAztecL1ContractsReturnType> {
284
290
  logger.info(`Deploying L1 contracts with config: ${jsonStringify(args)}`);
291
+ assertValidSlotDurations(args);
285
292
  if (args.initialValidators && args.initialValidators.length > 0 && args.existingTokenAddress) {
286
293
  throw new Error(
287
294
  'Cannot deploy with both initialValidators and existingTokenAddress. ' +
@@ -319,8 +326,9 @@ export async function deployAztecL1Contracts(
319
326
  // Use foundry-artifacts from l1-artifacts package
320
327
  const l1ContractsPath = prepareL1ContractsForDeployment();
321
328
 
329
+ const forgeBin = resolveFoundryBinary('forge');
322
330
  const FORGE_SCRIPT = 'script/deploy/DeployAztecL1Contracts.s.sol';
323
- await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
331
+ await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
324
332
 
325
333
  // Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
326
334
  const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
@@ -345,6 +353,8 @@ export async function deployAztecL1Contracts(
345
353
  ...(shouldVerify ? ['--verify'] : []),
346
354
  ];
347
355
  const forgeEnv = {
356
+ // Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
357
+ FORGE_BIN: forgeBin,
348
358
  // Env vars required by l1-contracts/script/deploy/DeploymentConfiguration.sol.
349
359
  NETWORK: getActiveNetworkName(),
350
360
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
@@ -589,6 +599,11 @@ export function getDeployRollupForUpgradeEnvVars(
589
599
  AZTEC_SLASH_AMOUNT_SMALL: args.slashAmountSmall.toString(),
590
600
  AZTEC_SLASH_AMOUNT_MEDIUM: args.slashAmountMedium.toString(),
591
601
  AZTEC_SLASH_AMOUNT_LARGE: args.slashAmountLarge.toString(),
602
+ AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE: args.entryQueueBootstrapValidatorSetSize.toString(),
603
+ AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE: args.entryQueueBootstrapFlushSize.toString(),
604
+ AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN: args.entryQueueFlushSizeMin.toString(),
605
+ AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT: args.entryQueueFlushSizeQuotient.toString(),
606
+ AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE: args.entryQueueMaxFlushSize.toString(),
592
607
  } as const;
593
608
  }
594
609
 
@@ -612,12 +627,15 @@ export const deployRollupForUpgrade = async (
612
627
  // Use foundry-artifacts from l1-artifacts package
613
628
  const l1ContractsPath = prepareL1ContractsForDeployment();
614
629
 
630
+ const forgeBin = resolveFoundryBinary('forge');
615
631
  const FORGE_SCRIPT = 'script/deploy/DeployRollupForUpgrade.s.sol';
616
- await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
632
+ await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
617
633
 
618
634
  const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
619
635
  const forgeArgs = [FORGE_SCRIPT, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
620
636
  const forgeEnv = {
637
+ // Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
638
+ FORGE_BIN: forgeBin,
621
639
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
622
640
  // Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
623
641
  REGISTRY_ADDRESS: registryAddress.toString(),
@@ -0,0 +1,57 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { accessSync, constants } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ function isExecutable(path: string): boolean {
7
+ try {
8
+ accessSync(path, constants.X_OK);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Locate a Foundry binary (`anvil`, `forge`, ...) without relying on the caller's PATH. Order:
17
+ * 1. `$<NAME>_BIN` (e.g. `$ANVIL_BIN`, `$FORGE_BIN`) — explicit override, e.g. for CI with a pinned
18
+ * version. Throws if set but not pointing at an executable, instead of silently falling back.
19
+ * 2. `~/.aztec/current/internal-bin/<name>` — where aztec-up installs it.
20
+ * 3. `~/.aztec/current/bin/aztec-<name>` — the publicly-exposed symlink.
21
+ * 4. `~/.foundry/bin/<name>` — standalone foundryup install.
22
+ * 5. `command -v <name>` — anything else on PATH.
23
+ *
24
+ * Throws with a directive message if none work.
25
+ */
26
+ export function resolveFoundryBinary(name: string): string {
27
+ const envVar = `${name.toUpperCase()}_BIN`;
28
+ const envBin = process.env[envVar];
29
+ if (envBin) {
30
+ if (!isExecutable(envBin)) {
31
+ throw new Error(`$${envVar} is set to ${envBin}, which does not exist or is not executable.`);
32
+ }
33
+ return envBin;
34
+ }
35
+
36
+ const candidates = [
37
+ join(homedir(), '.aztec', 'current', 'internal-bin', name),
38
+ join(homedir(), '.aztec', 'current', 'bin', `aztec-${name}`),
39
+ join(homedir(), '.foundry', 'bin', name),
40
+ ];
41
+ for (const path of candidates) {
42
+ if (isExecutable(path)) {
43
+ return path;
44
+ }
45
+ }
46
+
47
+ const which = spawnSync('sh', ['-c', `command -v ${name}`], { encoding: 'utf8' });
48
+ if (which.status === 0 && which.stdout.trim()) {
49
+ return which.stdout.trim();
50
+ }
51
+
52
+ throw new Error(
53
+ `${name} binary not found. Tried $${envVar}, ~/.aztec/current/internal-bin/${name}, ` +
54
+ `~/.aztec/current/bin/aztec-${name}, ~/.foundry/bin/${name}, and $PATH. ` +
55
+ `Install via \`aztec-up\` or set ${envVar} to a working binary.`,
56
+ );
57
+ }
@@ -73,7 +73,7 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
73
73
  gasLimitBufferPercentage: {
74
74
  description: 'How much to increase calculated gas limit by (percentage)',
75
75
  env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE',
76
- ...numberConfigHelper(20),
76
+ ...floatConfigHelper(20),
77
77
  },
78
78
  maxGwei: {
79
79
  description: 'Maximum gas price in gwei to be used for transactions.',
@@ -90,12 +90,12 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
90
90
  priorityFeeBumpPercentage: {
91
91
  description: 'How much to increase priority fee by each attempt (percentage)',
92
92
  env: 'L1_PRIORITY_FEE_BUMP_PERCENTAGE',
93
- ...numberConfigHelper(20),
93
+ ...floatConfigHelper(20),
94
94
  },
95
95
  priorityFeeRetryBumpPercentage: {
96
96
  description: 'How much to increase priority fee by each retry attempt (percentage)',
97
97
  env: 'L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE',
98
- ...numberConfigHelper(50),
98
+ ...floatConfigHelper(50),
99
99
  },
100
100
  minimumPriorityFeePerGas: {
101
101
  description:
@@ -791,9 +791,6 @@ export class L1FeeAnalyzer {
791
791
  });
792
792
  }
793
793
 
794
- /**
795
- * Gets the minimum value from an array of bigints
796
- */
797
794
  private minBigInt(values: bigint[]): bigint {
798
795
  if (values.length === 0) {
799
796
  return 0n;
@@ -801,3 +798,78 @@ export class L1FeeAnalyzer {
801
798
  return values.reduce((min, val) => (val < min ? val : min), values[0]);
802
799
  }
803
800
  }
801
+
802
+ /** Per-block fee data for one mined L1 block, used to diagnose whether a tx was underpriced for it. */
803
+ export interface WindowBlockFees {
804
+ blockNumber: bigint;
805
+ timestamp: bigint;
806
+ baseFeePerGas: bigint;
807
+ /** 75th percentile priority fee among the block's txs (gas-weighted, from eth_feeHistory). */
808
+ p75PriorityFee: bigint;
809
+ /** Minimum priority fee among all included txs — the inclusion bar for that block. */
810
+ minIncludedPriorityFee: bigint;
811
+ blockBlobsFull: boolean;
812
+ includedBlobCount: number;
813
+ }
814
+
815
+ /** Safety bound on how far back the window scan walks if the chain head is far ahead of the window. */
816
+ const MAX_WINDOW_SCAN_BLOCKS = 64;
817
+
818
+ /**
819
+ * Reads the already-mined L1 blocks whose timestamps fall in [windowStartS, windowEndS) — the L1
820
+ * inclusion window of an L2 slot — and extracts per-block fee data. Walks block headers to map the
821
+ * window to block numbers, then reads the priority-fee stats from a single eth_feeHistory call
822
+ * (percentile 0 is the cheapest included tx, i.e. the inclusion bar), so no transaction bodies are
823
+ * ever downloaded. Historical reads only, so it never waits on a future block. Returns the blocks in
824
+ * chronological order, [] on error, or the subset mined so far if the window is still in progress.
825
+ * Never throws.
826
+ */
827
+ export async function captureWindowBlockFees(
828
+ client: ViemClient,
829
+ windowStartS: bigint,
830
+ windowEndS: bigint,
831
+ ): Promise<WindowBlockFees[]> {
832
+ try {
833
+ let current = await client.getBlock({ blockTag: 'latest' });
834
+ const inWindow: (typeof current)[] = [];
835
+ let scanned = 0;
836
+ // Walk back from the head, collecting blocks inside the window and skipping any past its end,
837
+ // until we cross below the window start (or hit the safety cap / genesis).
838
+ while (current.timestamp >= windowStartS && scanned < MAX_WINDOW_SCAN_BLOCKS) {
839
+ if (current.timestamp < windowEndS) {
840
+ inWindow.push(current);
841
+ }
842
+ if (current.number === 0n) {
843
+ break;
844
+ }
845
+ current = await client.getBlock({ blockNumber: current.number - 1n });
846
+ scanned++;
847
+ }
848
+ if (inWindow.length === 0) {
849
+ return [];
850
+ }
851
+ inWindow.reverse();
852
+ const newest = inWindow[inWindow.length - 1];
853
+ const feeHistory = await client.getFeeHistory({
854
+ blockCount: Number(newest.number - inWindow[0].number) + 1,
855
+ blockNumber: newest.number,
856
+ rewardPercentiles: [0, 75],
857
+ });
858
+ return inWindow.map(block => {
859
+ const idx = Number(block.number - feeHistory.oldestBlock);
860
+ const [minIncludedPriorityFee, p75PriorityFee] = feeHistory.reward?.[idx] ?? [0n, 0n];
861
+ const blobsInBlock = block.blobGasUsed ? Number(block.blobGasUsed / GAS_PER_BLOB) : 0;
862
+ return {
863
+ blockNumber: block.number,
864
+ timestamp: block.timestamp,
865
+ baseFeePerGas: block.baseFeePerGas ?? 0n,
866
+ p75PriorityFee,
867
+ minIncludedPriorityFee,
868
+ blockBlobsFull: blobsInBlock >= getMaxBlobCapacity(block.timestamp),
869
+ includedBlobCount: blobsInBlock,
870
+ };
871
+ });
872
+ } catch {
873
+ return [];
874
+ }
875
+ }