@aztec/ethereum 0.0.1-commit.3fd054f6 → 0.0.1-commit.431c48d

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 (144) hide show
  1. package/dest/client.d.ts +19 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +52 -3
  4. package/dest/config.d.ts +15 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +29 -9
  7. package/dest/contracts/chain_state_override.d.ts +78 -0
  8. package/dest/contracts/chain_state_override.d.ts.map +1 -0
  9. package/dest/contracts/chain_state_override.js +137 -0
  10. package/dest/contracts/empire_base.d.ts +1 -3
  11. package/dest/contracts/empire_base.d.ts.map +1 -1
  12. package/dest/contracts/fee_asset_price_oracle.d.ts +6 -2
  13. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -1
  14. package/dest/contracts/fee_asset_price_oracle.js +11 -6
  15. package/dest/contracts/governance.d.ts +135 -35
  16. package/dest/contracts/governance.d.ts.map +1 -1
  17. package/dest/contracts/governance.js +226 -15
  18. package/dest/contracts/governance_proposer.d.ts +33 -4
  19. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  20. package/dest/contracts/governance_proposer.js +52 -13
  21. package/dest/contracts/gse.d.ts +4 -2
  22. package/dest/contracts/gse.d.ts.map +1 -1
  23. package/dest/contracts/gse.js +2 -2
  24. package/dest/contracts/inbox.d.ts +3 -3
  25. package/dest/contracts/inbox.d.ts.map +1 -1
  26. package/dest/contracts/inbox.js +12 -7
  27. package/dest/contracts/index.d.ts +3 -3
  28. package/dest/contracts/index.d.ts.map +1 -1
  29. package/dest/contracts/index.js +2 -2
  30. package/dest/contracts/multicall.d.ts +79 -13
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +117 -95
  33. package/dest/contracts/outbox.d.ts +27 -4
  34. package/dest/contracts/outbox.d.ts.map +1 -1
  35. package/dest/contracts/outbox.js +34 -7
  36. package/dest/contracts/rollup.d.ts +5418 -28
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +389 -95
  39. package/dest/contracts/slashing_proposer.d.ts +171 -0
  40. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  41. package/dest/contracts/slashing_proposer.js +753 -0
  42. package/dest/contracts/watch_event.d.ts +36 -0
  43. package/dest/contracts/watch_event.d.ts.map +1 -0
  44. package/dest/contracts/watch_event.js +66 -0
  45. package/dest/deploy_aztec_l1_contracts.d.ts +4 -7
  46. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  47. package/dest/deploy_aztec_l1_contracts.js +15 -8
  48. package/dest/foundry_binary.d.ts +13 -0
  49. package/dest/foundry_binary.d.ts.map +1 -0
  50. package/dest/foundry_binary.js +52 -0
  51. package/dest/generated/l1-contracts-defaults.d.ts +2 -2
  52. package/dest/generated/l1-contracts-defaults.js +2 -2
  53. package/dest/l1_artifacts.d.ts +14941 -16273
  54. package/dest/l1_artifacts.d.ts.map +1 -1
  55. package/dest/l1_artifacts.js +9 -24
  56. package/dest/l1_contract_addresses.d.ts +61 -65
  57. package/dest/l1_contract_addresses.d.ts.map +1 -1
  58. package/dest/l1_contract_addresses.js +101 -82
  59. package/dest/l1_reader.d.ts +3 -5
  60. package/dest/l1_reader.d.ts.map +1 -1
  61. package/dest/l1_reader.js +3 -6
  62. package/dest/l1_tx_utils/config.js +3 -3
  63. package/dest/l1_tx_utils/l1_tx_utils.d.ts +9 -1
  64. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  65. package/dest/l1_tx_utils/l1_tx_utils.js +115 -54
  66. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  67. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  68. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +49 -19
  69. package/dest/l1_tx_utils/tx_delayer.d.ts +5 -3
  70. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  71. package/dest/l1_tx_utils/tx_delayer.js +7 -5
  72. package/dest/publisher_manager.d.ts +18 -6
  73. package/dest/publisher_manager.d.ts.map +1 -1
  74. package/dest/publisher_manager.js +44 -7
  75. package/dest/queries.d.ts +13 -2
  76. package/dest/queries.d.ts.map +1 -1
  77. package/dest/queries.js +62 -3
  78. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  79. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  80. package/dest/test/blob_kzg_warmup.js +64 -0
  81. package/dest/test/chain_monitor.d.ts +34 -2
  82. package/dest/test/chain_monitor.d.ts.map +1 -1
  83. package/dest/test/chain_monitor.js +76 -6
  84. package/dest/test/eth_cheat_codes.d.ts +26 -3
  85. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  86. package/dest/test/eth_cheat_codes.js +61 -40
  87. package/dest/test/index.d.ts +2 -1
  88. package/dest/test/index.d.ts.map +1 -1
  89. package/dest/test/index.js +1 -0
  90. package/dest/test/recording_rpc_server.d.ts +18 -0
  91. package/dest/test/recording_rpc_server.d.ts.map +1 -0
  92. package/dest/test/recording_rpc_server.js +68 -0
  93. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  94. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  95. package/dest/test/rollup_cheat_codes.js +86 -7
  96. package/dest/test/start_anvil.d.ts +1 -1
  97. package/dest/test/start_anvil.d.ts.map +1 -1
  98. package/dest/test/start_anvil.js +41 -5
  99. package/dest/utils.d.ts +9 -2
  100. package/dest/utils.d.ts.map +1 -1
  101. package/dest/utils.js +45 -14
  102. package/package.json +6 -6
  103. package/src/client.ts +63 -1
  104. package/src/config.ts +44 -13
  105. package/src/contracts/chain_state_override.ts +200 -0
  106. package/src/contracts/empire_base.ts +0 -2
  107. package/src/contracts/fee_asset_price_oracle.ts +10 -5
  108. package/src/contracts/governance.ts +310 -10
  109. package/src/contracts/governance_proposer.ts +56 -13
  110. package/src/contracts/gse.ts +7 -2
  111. package/src/contracts/inbox.ts +13 -5
  112. package/src/contracts/index.ts +2 -2
  113. package/src/contracts/multicall.ts +179 -105
  114. package/src/contracts/outbox.ts +42 -8
  115. package/src/contracts/rollup.ts +429 -86
  116. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +119 -45
  117. package/src/contracts/watch_event.ts +110 -0
  118. package/src/deploy_aztec_l1_contracts.ts +19 -10
  119. package/src/foundry_binary.ts +57 -0
  120. package/src/generated/l1-contracts-defaults.ts +2 -2
  121. package/src/l1_artifacts.ts +12 -35
  122. package/src/l1_contract_addresses.ts +120 -94
  123. package/src/l1_reader.ts +4 -9
  124. package/src/l1_tx_utils/config.ts +3 -3
  125. package/src/l1_tx_utils/l1_tx_utils.ts +107 -50
  126. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +49 -13
  127. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  128. package/src/publisher_manager.ts +54 -18
  129. package/src/queries.ts +70 -4
  130. package/src/test/blob_kzg_warmup.ts +65 -0
  131. package/src/test/chain_monitor.ts +93 -5
  132. package/src/test/eth_cheat_codes.ts +57 -43
  133. package/src/test/index.ts +1 -0
  134. package/src/test/recording_rpc_server.ts +69 -0
  135. package/src/test/rollup_cheat_codes.ts +115 -6
  136. package/src/test/start_anvil.ts +38 -6
  137. package/src/utils.ts +41 -17
  138. package/dest/contracts/empire_slashing_proposer.d.ts +0 -69
  139. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  140. package/dest/contracts/empire_slashing_proposer.js +0 -216
  141. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  142. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  143. package/dest/contracts/tally_slashing_proposer.js +0 -320
  144. package/src/contracts/empire_slashing_proposer.ts +0 -265
@@ -3,11 +3,13 @@ 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';
9
+ import { createLogger } from '@aztec/foundation/log';
8
10
  import { hexToBuffer } from '@aztec/foundation/string';
9
11
  import { SlasherAbi } from '@aztec/l1-artifacts/SlasherAbi';
10
- import { TallySlashingProposerAbi } from '@aztec/l1-artifacts/TallySlashingProposerAbi';
12
+ import { SlashingProposerAbi } from '@aztec/l1-artifacts/SlashingProposerAbi';
11
13
 
12
14
  import {
13
15
  type GetContractReturnType,
@@ -18,14 +20,23 @@ import {
18
20
  getContract,
19
21
  } from 'viem';
20
22
 
23
+ import { type WatchContractEventOptions, watchContractEvent } from './watch_event.js';
24
+
21
25
  /**
22
- * Wrapper around the TallySlashingProposer contract that provides
26
+ * Wrapper around the SlashingProposer contract that provides
23
27
  * a TypeScript interface for interacting with the consensus-based slashing system.
24
28
  */
25
- export class TallySlashingProposerContract {
26
- private readonly contract: GetContractReturnType<typeof TallySlashingProposerAbi, ViemClient>;
29
+ export class SlashingProposerContract {
30
+ private readonly contract: GetContractReturnType<typeof SlashingProposerAbi, ViemClient>;
31
+ private readonly logger = createLogger('ethereum:slashing_proposer');
27
32
 
28
- public readonly type = 'tally' as const;
33
+ /**
34
+ * Slash target validators of the last round asked for. Safe to cache because a round's targets are the committees
35
+ * of epochs that had already ended when the round opened (see SLASH_OFFSET_IN_ROUNDS), and those committees are
36
+ * sampled from validator set and randao snapshots taken before the epoch started, so they cannot change while the
37
+ * round is live. The promise is cached rather than the result, so concurrent callers share one in-flight read.
38
+ */
39
+ private slashTargetValidators: { round: bigint; validators: Promise<EthAddress[]> } | undefined;
29
40
 
30
41
  constructor(
31
42
  public readonly client: ViemClient,
@@ -33,7 +44,7 @@ export class TallySlashingProposerContract {
33
44
  ) {
34
45
  this.contract = getContract({
35
46
  address: typeof address === 'string' ? address : address.toString(),
36
- abi: TallySlashingProposerAbi,
47
+ abi: SlashingProposerAbi,
37
48
  client,
38
49
  });
39
50
  }
@@ -66,6 +77,8 @@ export class TallySlashingProposerContract {
66
77
  return this.contract.read.EXECUTION_DELAY_IN_ROUNDS();
67
78
  }
68
79
 
80
+ /** Returns the slash amounts for the three slash unit levels. Immutable on the contract, so memoized. */
81
+ @memoize
69
82
  public getSlashingAmounts(): Promise<[bigint, bigint, bigint]> {
70
83
  return Promise.all([
71
84
  this.contract.read.SLASH_AMOUNT_SMALL(),
@@ -136,12 +149,12 @@ export class TallySlashingProposerContract {
136
149
 
137
150
  /** Tries to extract a VoteCast event from the given logs. */
138
151
  public tryExtractVoteCastEvent(logs: Log[]) {
139
- return tryExtractEvent(logs, this.address.toString(), TallySlashingProposerAbi, 'VoteCast');
152
+ return tryExtractEvent(logs, this.address.toString(), SlashingProposerAbi, 'VoteCast');
140
153
  }
141
154
 
142
155
  /** Tries to extract a RoundExecuted event from the given logs. */
143
156
  public tryExtractRoundExecutedEvent(logs: Log[]) {
144
- return tryExtractEvent(logs, this.address.toString(), TallySlashingProposerAbi, 'RoundExecuted');
157
+ return tryExtractEvent(logs, this.address.toString(), SlashingProposerAbi, 'RoundExecuted');
145
158
  }
146
159
 
147
160
  /**
@@ -161,9 +174,9 @@ export class TallySlashingProposerContract {
161
174
 
162
175
  return {
163
176
  to: this.contract.address,
164
- abi: TallySlashingProposerAbi,
177
+ abi: SlashingProposerAbi,
165
178
  data: encodeFunctionData({
166
- abi: TallySlashingProposerAbi,
179
+ abi: SlashingProposerAbi,
167
180
  functionName: 'vote',
168
181
  args: [votes, signature.toViemSignature()],
169
182
  }),
@@ -173,7 +186,7 @@ export class TallySlashingProposerContract {
173
186
  /** Returns the typed data definition to EIP712-sign for voting */
174
187
  public buildVoteTypedData(votes: Hex, slot: SlotNumber): TypedDataDefinition {
175
188
  const domain = {
176
- name: 'TallySlashingProposer',
189
+ name: 'SlashingProposer',
177
190
  version: '1',
178
191
  chainId: this.client.chain.id,
179
192
  verifyingContract: this.contract.address,
@@ -209,9 +222,9 @@ export class TallySlashingProposerContract {
209
222
  public buildVoteRequestWithSignature(votes: Hex, signature: { v: number; r: Hex; s: Hex }): L1TxRequest {
210
223
  return {
211
224
  to: this.contract.address,
212
- abi: TallySlashingProposerAbi,
225
+ abi: SlashingProposerAbi,
213
226
  data: encodeFunctionData({
214
- abi: TallySlashingProposerAbi,
227
+ abi: SlashingProposerAbi,
215
228
  functionName: 'vote',
216
229
  args: [votes, signature],
217
230
  }),
@@ -227,75 +240,136 @@ export class TallySlashingProposerContract {
227
240
  public buildExecuteRoundRequest(round: bigint, committees: EthAddress[][]): L1TxRequest {
228
241
  return {
229
242
  to: this.contract.address,
230
- abi: mergeAbis([TallySlashingProposerAbi, SlasherAbi]),
243
+ abi: mergeAbis([SlashingProposerAbi, SlasherAbi]),
231
244
  data: encodeFunctionData({
232
- abi: TallySlashingProposerAbi,
245
+ abi: SlashingProposerAbi,
233
246
  functionName: 'executeRound',
234
247
  args: [round, committees.map(c => c.map(addr => addr.toString()))],
235
248
  }),
236
249
  };
237
250
  }
238
251
 
252
+ /**
253
+ * Returns the validators eligible to be voted against in a round, in the order votes encode them. Cached for the
254
+ * last round asked for: reading them runs a committee sampling simulation on L1, and every vote of a round decodes
255
+ * against the same list, so a caller walking a round's votes would otherwise repeat that call per vote. Only the
256
+ * last round is kept since callers move forward round by round.
257
+ */
258
+ public getSlashTargetValidators(round: bigint): Promise<EthAddress[]> {
259
+ const cached = this.slashTargetValidators;
260
+ if (cached?.round === round) {
261
+ return cached.validators;
262
+ }
263
+
264
+ const entry = { round, validators: this.fetchSlashTargetValidators(round) };
265
+ this.slashTargetValidators = entry;
266
+ // A failed read must not stay cached, or every later vote of the round would replay the same rejection
267
+ entry.validators.catch(() => {
268
+ if (this.slashTargetValidators === entry) {
269
+ this.slashTargetValidators = undefined;
270
+ }
271
+ });
272
+ return entry.validators;
273
+ }
274
+
275
+ private async fetchSlashTargetValidators(round: bigint): Promise<EthAddress[]> {
276
+ const { result } = await this.contract.simulate.getSlashTargetCommittees([round]);
277
+ return result.flat().map(validator => EthAddress.fromString(validator));
278
+ }
279
+
280
+ /**
281
+ * Returns the slash amount voted for each target validator by a single vote of a round.
282
+ * @param index - Position of the vote within the round, from 0 (inclusive) to the round's vote count (exclusive)
283
+ */
284
+ public async getVoteAt(round: bigint, index: bigint): Promise<SlashVoteTarget[]> {
285
+ const [validators, vote, slashAmounts] = await Promise.all([
286
+ this.getSlashTargetValidators(round),
287
+ this.contract.read.getVotes([round, index]),
288
+ this.getSlashingAmounts(),
289
+ ]);
290
+ return decodeVote(vote, validators, slashAmounts);
291
+ }
292
+
239
293
  /** Returns the last vote emitted for a given round */
240
294
  public async getLastVote(round: bigint) {
241
295
  const { voteCount } = await this.getRound(round);
242
- const validators = (await this.contract.simulate.getSlashTargetCommittees([round])).result.flat();
243
- const vote = await this.contract.read.getVotes([round, voteCount - 1n]);
244
- const decoded = decodeSlashConsensusVotes(hexToBuffer(vote));
245
- const slashAmounts = await this.getSlashingAmounts();
246
- return decoded
247
- .map((units, i) => ({
248
- validator: EthAddress.fromString(validators[i]),
249
- slashAmount: slashAmounts[units - 1] ?? 0n,
250
- }))
251
- .filter(v => v.slashAmount > 0n);
296
+ return await this.getVoteAt(round, voteCount - 1n);
252
297
  }
253
298
 
254
299
  /**
255
- * Listen for VoteCast events
300
+ * Listen for VoteCast events. Events are delivered by polling `eth_getLogs`: a reorg may re-emit them and
301
+ * removals are never reported, and events mined within roughly one polling interval of subscribing may be missed.
256
302
  * @param callback - Callback function to handle vote cast events
257
303
  * @returns Unwatch function
258
304
  */
259
- public listenToVoteCast(callback: (args: { round: bigint; proposer: string }) => void): () => void {
260
- return this.contract.watchEvent.VoteCast(
261
- {},
305
+ public listenToVoteCast(
306
+ callback: (args: { round: bigint; proposer: string }) => unknown,
307
+ options?: WatchContractEventOptions,
308
+ ): () => void {
309
+ return watchContractEvent(
310
+ this.client,
311
+ this.logger,
262
312
  {
263
- onLogs: logs => {
264
- for (const log of logs) {
265
- const { round, proposer } = log.args;
266
- if (round !== undefined && proposer) {
267
- callback({ round, proposer });
268
- }
313
+ address: this.contract.address,
314
+ abi: SlashingProposerAbi,
315
+ eventName: 'VoteCast',
316
+ onLog: log => {
317
+ const { round, proposer } = log.args;
318
+ if (round !== undefined && proposer) {
319
+ return callback({ round, proposer });
269
320
  }
270
321
  },
271
322
  },
323
+ options,
272
324
  );
273
325
  }
274
326
 
275
327
  /**
276
- * Listen for RoundExecuted events
328
+ * Listen for RoundExecuted events. Events are delivered by polling `eth_getLogs`: a reorg may re-emit them and
329
+ * removals are never reported, and events mined within roughly one polling interval of subscribing may be missed.
277
330
  * @param callback - Callback function to handle round executed events
278
331
  * @returns Unwatch function
279
332
  */
280
333
  public listenToRoundExecuted(
281
- callback: (args: { round: bigint; slashCount: bigint; l1BlockHash: Hex }) => void,
334
+ callback: (args: { round: bigint; slashCount: bigint; l1BlockHash: Hex }) => unknown,
335
+ options?: WatchContractEventOptions,
282
336
  ): () => void {
283
- return this.contract.watchEvent.RoundExecuted(
284
- {},
337
+ return watchContractEvent(
338
+ this.client,
339
+ this.logger,
285
340
  {
286
- onLogs: logs => {
287
- for (const log of logs) {
288
- const { round, slashCount } = log.args;
289
- if (round !== undefined && slashCount !== undefined) {
290
- callback({ round, slashCount, l1BlockHash: log.blockHash });
291
- }
341
+ address: this.contract.address,
342
+ abi: SlashingProposerAbi,
343
+ eventName: 'RoundExecuted',
344
+ onLog: log => {
345
+ const { round, slashCount } = log.args;
346
+ if (round !== undefined && slashCount !== undefined) {
347
+ return callback({ round, slashCount, l1BlockHash: log.blockHash });
292
348
  }
293
349
  },
294
350
  },
351
+ options,
295
352
  );
296
353
  }
297
354
  }
298
355
 
356
+ /**
357
+ * A validator targeted by a slashing vote, with the amount voted. The position is the validator's index in the
358
+ * round's flattened slash target committees — the unit the contract tallies quorum by. A validator sitting in
359
+ * several of the round's committees holds several positions, each with its own tally.
360
+ */
361
+ export type SlashVoteTarget = { validator: EthAddress; slashAmount: bigint; position: number };
362
+
363
+ function decodeVote(vote: Hex, validators: EthAddress[], slashAmounts: [bigint, bigint, bigint]): SlashVoteTarget[] {
364
+ return decodeSlashConsensusVotes(hexToBuffer(vote))
365
+ .map((units, position) => ({
366
+ validator: validators[position],
367
+ slashAmount: slashAmounts[units - 1] ?? 0n,
368
+ position,
369
+ }))
370
+ .filter(v => v.slashAmount > 0n);
371
+ }
372
+
299
373
  /**
300
374
  * Decodes a Buffer containing slash votes back into an array of numbers.
301
375
  * Each vote is represented as a 2-bit value (0, 1, 2, or 3) representing slashing units.
@@ -0,0 +1,110 @@
1
+ import type { Logger } from '@aztec/foundation/log';
2
+ import { RunningPromise } from '@aztec/foundation/promise';
3
+
4
+ import type { Abi, ContractEventName, GetContractEventsReturnType, Hex } from 'viem';
5
+
6
+ import type { ViemClient } from '../types.js';
7
+
8
+ /** Default maximum number of blocks queried per `eth_getLogs` request. */
9
+ const DEFAULT_MAX_BLOCK_RANGE = 100;
10
+
11
+ /** Options for {@link watchContractEvent}. */
12
+ export type WatchContractEventOptions = {
13
+ /** How often to poll for new L1 blocks, in ms. Defaults to the client's polling interval. */
14
+ pollingIntervalMs?: number;
15
+ /** Maximum number of blocks per `eth_getLogs` request; wider catch-up ranges are split into chunks. */
16
+ maxBlockRange?: number;
17
+ };
18
+
19
+ /** Event identity and handler for {@link watchContractEvent}. */
20
+ export type WatchContractEventParameters<
21
+ TAbi extends Abi,
22
+ TEventName extends ContractEventName<TAbi>,
23
+ TStrict extends boolean | undefined = undefined,
24
+ > = {
25
+ address: Hex;
26
+ abi: TAbi;
27
+ eventName: TEventName;
28
+ /** Whether logs must decode against the event's indexed/non-indexed arguments (defaults to false). */
29
+ strict?: TStrict;
30
+ /** Called once per event log. Errors, thrown or from a returned promise, are caught and logged. */
31
+ onLog: (log: GetContractEventsReturnType<TAbi, TEventName, TStrict>[number]) => unknown;
32
+ };
33
+
34
+ /**
35
+ * Watches for contract events by polling `eth_blockNumber` (cached by viem per client) and fetching new logs with
36
+ * ranged `eth_getLogs` requests, never installing server-side filters. Server-side filters are unusable against
37
+ * real-world endpoints: a load balancer routes polls to backends that never saw the filter, and providers report a
38
+ * purged filter with error codes viem does not recognize as "filter gone", so viem's own watchers either churn
39
+ * recreating filters or poll a dead filter id forever while silently missing events.
40
+ *
41
+ * The block that is latest on the first poll becomes the starting cursor, so its events are reported and events from
42
+ * earlier blocks are not. Requests never span more than `maxBlockRange` blocks: catching up after downtime is
43
+ * chunked into multiple requests, and the cursor advances per successful chunk so a failure retries from where it
44
+ * left off on the next tick. A reorg may re-emit events and removals are never reported.
45
+ *
46
+ * @returns A function that stops the watcher.
47
+ */
48
+ export function watchContractEvent<
49
+ TAbi extends Abi,
50
+ TEventName extends ContractEventName<TAbi>,
51
+ TStrict extends boolean | undefined = undefined,
52
+ >(
53
+ client: ViemClient,
54
+ logger: Logger,
55
+ params: WatchContractEventParameters<TAbi, TEventName, TStrict>,
56
+ options: WatchContractEventOptions = {},
57
+ ): () => void {
58
+ const { address, abi, eventName, strict, onLog } = params;
59
+ const pollingIntervalMs = options.pollingIntervalMs ?? client.pollingInterval;
60
+ const maxBlockRange = BigInt(options.maxBlockRange ?? DEFAULT_MAX_BLOCK_RANGE);
61
+
62
+ const logHandlerError = (err: unknown) => logger.error(`Error handling ${eventName} L1 event`, err);
63
+ const deliver = (log: GetContractEventsReturnType<TAbi, TEventName, TStrict>[number]) => {
64
+ try {
65
+ const result = onLog(log);
66
+ if (result instanceof Promise) {
67
+ result.catch(logHandlerError);
68
+ }
69
+ } catch (err) {
70
+ logHandlerError(err);
71
+ }
72
+ };
73
+
74
+ let stopped = false;
75
+ let nextBlock: bigint | undefined;
76
+
77
+ const poll = async () => {
78
+ const latestBlock = await client.getBlockNumber();
79
+ if (nextBlock === undefined) {
80
+ nextBlock = latestBlock;
81
+ return;
82
+ }
83
+ let fromBlock: bigint = nextBlock;
84
+ while (fromBlock <= latestBlock && !stopped) {
85
+ const chunkEnd = fromBlock + maxBlockRange - 1n;
86
+ const toBlock = chunkEnd < latestBlock ? chunkEnd : latestBlock;
87
+ const logs = await client.getContractEvents<TAbi, TEventName, TStrict, bigint, bigint>({
88
+ address,
89
+ abi,
90
+ eventName,
91
+ strict,
92
+ fromBlock,
93
+ toBlock,
94
+ });
95
+ for (const log of logs) {
96
+ deliver(log);
97
+ }
98
+ fromBlock = toBlock + 1n;
99
+ nextBlock = fromBlock;
100
+ }
101
+ };
102
+
103
+ const handlePollError = (err: unknown) => logger.warn(`Error polling for ${eventName} L1 events`, { err });
104
+ const runner = new RunningPromise(poll, logger, pollingIntervalMs, handlePollError).start();
105
+
106
+ return () => {
107
+ stopped = true;
108
+ void runner.stop();
109
+ };
110
+ }
@@ -19,9 +19,10 @@ 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
 
@@ -93,11 +94,16 @@ function runProcess<T>(
93
94
 
94
95
  // Covers an edge where where we may have a cached BlobLib that is not meant for production.
95
96
  // 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) {
97
+ async function maybeForgeForceProductionBuild(
98
+ forgeBin: string,
99
+ l1ContractsPath: string,
100
+ script: string,
101
+ chainId: number,
102
+ ) {
97
103
  if (chainId === mainnet.id) {
98
104
  logger.info(`Recompiling ${script} with production profile for mainnet deployment`);
99
105
  logger.info('This may take a minute but ensures production BlobLib is used.');
100
- await runProcess('forge', ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
106
+ await runProcess(forgeBin, ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
101
107
  }
102
108
  }
103
109
 
@@ -230,7 +236,6 @@ export function computeValidatorData(operator: Operator): ValidatorJson {
230
236
  export interface RollupUpgradeAddresses {
231
237
  rollupAddress: string;
232
238
  verifierAddress: string;
233
- slashFactoryAddress: string;
234
239
  inboxAddress: string;
235
240
  outboxAddress: string;
236
241
  feeJuicePortalAddress: string;
@@ -243,7 +248,6 @@ export interface RollupUpgradeAddresses {
243
248
  export interface ForgeRollupUpgradeResult {
244
249
  rollupAddress: Hex;
245
250
  verifierAddress: Hex;
246
- slashFactoryAddress: Hex;
247
251
  inboxAddress: Hex;
248
252
  outboxAddress: Hex;
249
253
  feeJuicePortalAddress: Hex;
@@ -284,6 +288,7 @@ export async function deployAztecL1Contracts(
284
288
  args: DeployAztecL1ContractsArgs,
285
289
  ): Promise<DeployAztecL1ContractsReturnType> {
286
290
  logger.info(`Deploying L1 contracts with config: ${jsonStringify(args)}`);
291
+ assertValidSlotDurations(args);
287
292
  if (args.initialValidators && args.initialValidators.length > 0 && args.existingTokenAddress) {
288
293
  throw new Error(
289
294
  'Cannot deploy with both initialValidators and existingTokenAddress. ' +
@@ -321,8 +326,9 @@ export async function deployAztecL1Contracts(
321
326
  // Use foundry-artifacts from l1-artifacts package
322
327
  const l1ContractsPath = prepareL1ContractsForDeployment();
323
328
 
329
+ const forgeBin = resolveFoundryBinary('forge');
324
330
  const FORGE_SCRIPT = 'script/deploy/DeployAztecL1Contracts.s.sol';
325
- await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
331
+ await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
326
332
 
327
333
  // Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
328
334
  const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
@@ -347,6 +353,8 @@ export async function deployAztecL1Contracts(
347
353
  ...(shouldVerify ? ['--verify'] : []),
348
354
  ];
349
355
  const forgeEnv = {
356
+ // Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
357
+ FORGE_BIN: forgeBin,
350
358
  // Env vars required by l1-contracts/script/deploy/DeploymentConfiguration.sol.
351
359
  NETWORK: getActiveNetworkName(),
352
360
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
@@ -403,7 +411,6 @@ export async function deployAztecL1Contracts(
403
411
  governanceProposerAddress: EthAddress.fromString(result.governanceProposerAddress),
404
412
  governanceAddress: EthAddress.fromString(result.governanceAddress),
405
413
  stakingAssetAddress: EthAddress.fromString(result.stakingAssetAddress),
406
- slashFactoryAddress: result.slashFactoryAddress ? EthAddress.fromString(result.slashFactoryAddress) : undefined,
407
414
  feeAssetHandlerAddress: result.feeAssetHandlerAddress
408
415
  ? EthAddress.fromString(result.feeAssetHandlerAddress)
409
416
  : undefined,
@@ -585,7 +592,7 @@ export function getDeployRollupForUpgradeEnvVars(
585
592
  AZTEC_EXIT_DELAY_SECONDS: args.exitDelaySeconds.toString(),
586
593
  AZTEC_PROVING_COST_PER_MANA: args.provingCostPerMana.toString(),
587
594
  AZTEC_INITIAL_ETH_PER_FEE_ASSET: args.initialEthPerFeeAsset.toString(),
588
- AZTEC_SLASHER_FLAVOR: args.slasherFlavor,
595
+ AZTEC_SLASHER_ENABLED: args.slasherEnabled ? 'true' : 'false',
589
596
  AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS: args.slashingRoundSizeInEpochs.toString(),
590
597
  AZTEC_SLASHING_QUORUM: args.slashingQuorum?.toString(),
591
598
  AZTEC_SLASHING_OFFSET_IN_ROUNDS: args.slashingOffsetInRounds.toString(),
@@ -615,12 +622,15 @@ export const deployRollupForUpgrade = async (
615
622
  // Use foundry-artifacts from l1-artifacts package
616
623
  const l1ContractsPath = prepareL1ContractsForDeployment();
617
624
 
625
+ const forgeBin = resolveFoundryBinary('forge');
618
626
  const FORGE_SCRIPT = 'script/deploy/DeployRollupForUpgrade.s.sol';
619
- await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
627
+ await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
620
628
 
621
629
  const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
622
630
  const forgeArgs = [FORGE_SCRIPT, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
623
631
  const forgeEnv = {
632
+ // Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
633
+ FORGE_BIN: forgeBin,
624
634
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
625
635
  // Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
626
636
  REGISTRY_ADDRESS: registryAddress.toString(),
@@ -645,6 +655,5 @@ export const deployRollupForUpgrade = async (
645
655
 
646
656
  return {
647
657
  rollup,
648
- slashFactoryAddress: result.slashFactoryAddress,
649
658
  };
650
659
  };
@@ -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
+ }
@@ -13,12 +13,12 @@ export const l1ContractsDefaultEnv = {
13
13
  AZTEC_EJECTION_THRESHOLD: 50000000000000000000,
14
14
  AZTEC_LOCAL_EJECTION_THRESHOLD: 98000000000000000000,
15
15
  AZTEC_EXIT_DELAY_SECONDS: 172800,
16
- AZTEC_INBOX_LAG: 1,
16
+ AZTEC_INBOX_LAG: 2,
17
17
  AZTEC_PROOF_SUBMISSION_EPOCHS: 1,
18
18
  AZTEC_MANA_TARGET: 100000000,
19
19
  AZTEC_PROVING_COST_PER_MANA: 100,
20
20
  AZTEC_INITIAL_ETH_PER_FEE_ASSET: 10000000,
21
- AZTEC_SLASHER_FLAVOR: 'tally',
21
+ AZTEC_SLASHER_ENABLED: true,
22
22
  AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS: 4,
23
23
  AZTEC_SLASHING_LIFETIME_IN_ROUNDS: 5,
24
24
  AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS: 0,
@@ -3,10 +3,6 @@ import {
3
3
  CoinIssuerBytecode,
4
4
  DateGatedRelayerAbi,
5
5
  DateGatedRelayerBytecode,
6
- EmpireSlasherDeploymentExtLibAbi,
7
- EmpireSlasherDeploymentExtLibBytecode,
8
- EmpireSlashingProposerAbi,
9
- EmpireSlashingProposerBytecode,
10
6
  FeeAssetHandlerAbi,
11
7
  FeeAssetHandlerBytecode,
12
8
  FeeJuicePortalAbi,
@@ -42,16 +38,14 @@ import {
42
38
  RollupLinkReferences,
43
39
  RollupOperationsExtLibAbi,
44
40
  RollupOperationsExtLibBytecode,
45
- SlashFactoryAbi,
46
- SlashFactoryBytecode,
47
41
  SlasherAbi,
48
42
  SlasherBytecode,
43
+ SlasherDeploymentExtLibAbi,
44
+ SlasherDeploymentExtLibBytecode,
45
+ SlashingProposerAbi,
46
+ SlashingProposerBytecode,
49
47
  StakingAssetHandlerAbi,
50
48
  StakingAssetHandlerBytecode,
51
- TallySlasherDeploymentExtLibAbi,
52
- TallySlasherDeploymentExtLibBytecode,
53
- TallySlashingProposerAbi,
54
- TallySlashingProposerBytecode,
55
49
  TestERC20Abi,
56
50
  TestERC20Bytecode,
57
51
  ValidatorOperationsExtLibAbi,
@@ -107,15 +101,10 @@ export const RollupArtifact = {
107
101
  contractAbi: RewardExtLibAbi,
108
102
  contractBytecode: RewardExtLibBytecode as Hex,
109
103
  },
110
- TallySlasherDeploymentExtLib: {
111
- name: 'TallySlasherDeploymentExtLib',
112
- contractAbi: TallySlasherDeploymentExtLibAbi,
113
- contractBytecode: TallySlasherDeploymentExtLibBytecode as Hex,
114
- },
115
- EmpireSlasherDeploymentExtLib: {
116
- name: 'EmpireSlasherDeploymentExtLib',
117
- contractAbi: EmpireSlasherDeploymentExtLibAbi,
118
- contractBytecode: EmpireSlasherDeploymentExtLibBytecode as Hex,
104
+ SlasherDeploymentExtLib: {
105
+ name: 'SlasherDeploymentExtLib',
106
+ contractAbi: SlasherDeploymentExtLibAbi,
107
+ contractBytecode: SlasherDeploymentExtLibBytecode as Hex,
119
108
  },
120
109
  },
121
110
  },
@@ -175,22 +164,10 @@ export const SlasherArtifact = {
175
164
  contractBytecode: SlasherBytecode as Hex,
176
165
  };
177
166
 
178
- export const EmpireSlashingProposerArtifact = {
179
- name: 'EmpireSlashingProposer',
180
- contractAbi: EmpireSlashingProposerAbi,
181
- contractBytecode: EmpireSlashingProposerBytecode as Hex,
182
- };
183
-
184
- export const TallySlashingProposerArtifact = {
185
- name: 'TallySlashingProposer',
186
- contractAbi: TallySlashingProposerAbi,
187
- contractBytecode: TallySlashingProposerBytecode as Hex,
188
- };
189
-
190
- export const SlashFactoryArtifact = {
191
- name: 'SlashFactory',
192
- contractAbi: SlashFactoryAbi,
193
- contractBytecode: SlashFactoryBytecode as Hex,
167
+ export const SlashingProposerArtifact = {
168
+ name: 'SlashingProposer',
169
+ contractAbi: SlashingProposerAbi,
170
+ contractBytecode: SlashingProposerBytecode as Hex,
194
171
  };
195
172
 
196
173
  export const RegisterNewRollupVersionPayloadArtifact = {