@aztec/ethereum 0.0.1-commit.f2ce05ee → 0.0.1-commit.f5a9928

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 (181) hide show
  1. package/dest/client.d.ts +27 -3
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +63 -8
  4. package/dest/config.d.ts +17 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +38 -10
  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/fee_asset_price_oracle.d.ts +105 -0
  11. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
  12. package/dest/contracts/fee_asset_price_oracle.js +656 -0
  13. package/dest/contracts/governance.d.ts +135 -35
  14. package/dest/contracts/governance.d.ts.map +1 -1
  15. package/dest/contracts/governance.js +229 -18
  16. package/dest/contracts/governance_proposer.d.ts +33 -2
  17. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  18. package/dest/contracts/governance_proposer.js +54 -6
  19. package/dest/contracts/gse.d.ts +4 -2
  20. package/dest/contracts/gse.d.ts.map +1 -1
  21. package/dest/contracts/gse.js +2 -2
  22. package/dest/contracts/inbox.d.ts +3 -3
  23. package/dest/contracts/inbox.d.ts.map +1 -1
  24. package/dest/contracts/inbox.js +12 -7
  25. package/dest/contracts/index.d.ts +4 -3
  26. package/dest/contracts/index.d.ts.map +1 -1
  27. package/dest/contracts/index.js +3 -2
  28. package/dest/contracts/multicall.d.ts +127 -12
  29. package/dest/contracts/multicall.d.ts.map +1 -1
  30. package/dest/contracts/multicall.js +201 -94
  31. package/dest/contracts/outbox.d.ts +27 -4
  32. package/dest/contracts/outbox.d.ts.map +1 -1
  33. package/dest/contracts/outbox.js +34 -7
  34. package/dest/contracts/registry.d.ts +3 -1
  35. package/dest/contracts/registry.d.ts.map +1 -1
  36. package/dest/contracts/registry.js +30 -1
  37. package/dest/contracts/rollup.d.ts +5437 -34
  38. package/dest/contracts/rollup.d.ts.map +1 -1
  39. package/dest/contracts/rollup.js +448 -104
  40. package/dest/contracts/slashing_proposer.d.ts +171 -0
  41. package/dest/contracts/slashing_proposer.d.ts.map +1 -0
  42. package/dest/contracts/slashing_proposer.js +753 -0
  43. package/dest/contracts/watch_event.d.ts +36 -0
  44. package/dest/contracts/watch_event.d.ts.map +1 -0
  45. package/dest/contracts/watch_event.js +66 -0
  46. package/dest/deploy_aztec_l1_contracts.d.ts +5 -9
  47. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  48. package/dest/deploy_aztec_l1_contracts.js +48 -27
  49. package/dest/deploy_l1_contract.js +3 -3
  50. package/dest/foundry_binary.d.ts +13 -0
  51. package/dest/foundry_binary.d.ts.map +1 -0
  52. package/dest/foundry_binary.js +52 -0
  53. package/dest/generated/l1-contracts-defaults.d.ts +3 -3
  54. package/dest/generated/l1-contracts-defaults.js +3 -3
  55. package/dest/l1_artifacts.d.ts +15524 -15847
  56. package/dest/l1_artifacts.d.ts.map +1 -1
  57. package/dest/l1_artifacts.js +9 -24
  58. package/dest/l1_contract_addresses.d.ts +61 -65
  59. package/dest/l1_contract_addresses.d.ts.map +1 -1
  60. package/dest/l1_contract_addresses.js +101 -82
  61. package/dest/l1_reader.d.ts +5 -5
  62. package/dest/l1_reader.d.ts.map +1 -1
  63. package/dest/l1_reader.js +9 -7
  64. package/dest/l1_tx_utils/config.d.ts +7 -1
  65. package/dest/l1_tx_utils/config.d.ts.map +1 -1
  66. package/dest/l1_tx_utils/config.js +17 -4
  67. package/dest/l1_tx_utils/factory.d.ts +18 -10
  68. package/dest/l1_tx_utils/factory.d.ts.map +1 -1
  69. package/dest/l1_tx_utils/factory.js +17 -7
  70. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +1 -1
  71. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +1 -1
  72. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts +15 -15
  73. package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts.map +1 -1
  74. package/dest/l1_tx_utils/forwarder_l1_tx_utils.js +9 -15
  75. package/dest/l1_tx_utils/index-blobs.d.ts +3 -3
  76. package/dest/l1_tx_utils/index-blobs.d.ts.map +1 -1
  77. package/dest/l1_tx_utils/index-blobs.js +2 -2
  78. package/dest/l1_tx_utils/index.d.ts +2 -1
  79. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  80. package/dest/l1_tx_utils/index.js +1 -0
  81. package/dest/l1_tx_utils/l1_tx_utils.d.ts +26 -7
  82. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  83. package/dest/l1_tx_utils/l1_tx_utils.js +156 -79
  84. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  85. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  86. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +57 -23
  87. package/dest/l1_tx_utils/tx_delayer.d.ts +58 -0
  88. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -0
  89. package/dest/{test → l1_tx_utils}/tx_delayer.js +67 -37
  90. package/dest/publisher_manager.d.ts +34 -8
  91. package/dest/publisher_manager.d.ts.map +1 -1
  92. package/dest/publisher_manager.js +119 -8
  93. package/dest/queries.d.ts +13 -2
  94. package/dest/queries.d.ts.map +1 -1
  95. package/dest/queries.js +62 -3
  96. package/dest/test/blob_kzg_warmup.d.ts +19 -0
  97. package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
  98. package/dest/test/blob_kzg_warmup.js +64 -0
  99. package/dest/test/chain_monitor.d.ts +55 -4
  100. package/dest/test/chain_monitor.d.ts.map +1 -1
  101. package/dest/test/chain_monitor.js +104 -3
  102. package/dest/test/eth_cheat_codes.d.ts +31 -6
  103. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  104. package/dest/test/eth_cheat_codes.js +67 -44
  105. package/dest/test/index.d.ts +2 -3
  106. package/dest/test/index.d.ts.map +1 -1
  107. package/dest/test/index.js +1 -2
  108. package/dest/test/recording_rpc_server.d.ts +18 -0
  109. package/dest/test/recording_rpc_server.d.ts.map +1 -0
  110. package/dest/test/recording_rpc_server.js +68 -0
  111. package/dest/test/rollup_cheat_codes.d.ts +60 -3
  112. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  113. package/dest/test/rollup_cheat_codes.js +86 -7
  114. package/dest/test/start_anvil.d.ts +23 -3
  115. package/dest/test/start_anvil.d.ts.map +1 -1
  116. package/dest/test/start_anvil.js +182 -32
  117. package/dest/test/upgrade_utils.js +2 -2
  118. package/dest/utils.d.ts +9 -2
  119. package/dest/utils.d.ts.map +1 -1
  120. package/dest/utils.js +59 -24
  121. package/package.json +6 -8
  122. package/src/client.ts +72 -2
  123. package/src/config.ts +58 -14
  124. package/src/contracts/chain_state_override.ts +200 -0
  125. package/src/contracts/fee_asset_price_oracle.ts +285 -0
  126. package/src/contracts/governance.ts +313 -13
  127. package/src/contracts/governance_proposer.ts +58 -9
  128. package/src/contracts/gse.ts +7 -2
  129. package/src/contracts/inbox.ts +13 -5
  130. package/src/contracts/index.ts +3 -2
  131. package/src/contracts/multicall.ts +243 -105
  132. package/src/contracts/outbox.ts +42 -8
  133. package/src/contracts/registry.ts +31 -1
  134. package/src/contracts/rollup.ts +507 -109
  135. package/src/contracts/{tally_slashing_proposer.ts → slashing_proposer.ts} +119 -45
  136. package/src/contracts/watch_event.ts +110 -0
  137. package/src/deploy_aztec_l1_contracts.ts +75 -39
  138. package/src/deploy_l1_contract.ts +3 -3
  139. package/src/foundry_binary.ts +57 -0
  140. package/src/generated/l1-contracts-defaults.ts +3 -3
  141. package/src/l1_artifacts.ts +12 -35
  142. package/src/l1_contract_addresses.ts +120 -94
  143. package/src/l1_reader.ts +17 -10
  144. package/src/l1_tx_utils/config.ts +23 -3
  145. package/src/l1_tx_utils/factory.ts +31 -31
  146. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +1 -1
  147. package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +1 -1
  148. package/src/l1_tx_utils/forwarder_l1_tx_utils.ts +43 -54
  149. package/src/l1_tx_utils/index-blobs.ts +2 -2
  150. package/src/l1_tx_utils/index.ts +1 -0
  151. package/src/l1_tx_utils/l1_tx_utils.ts +149 -64
  152. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +57 -17
  153. package/src/{test → l1_tx_utils}/tx_delayer.ts +93 -56
  154. package/src/publisher_manager.ts +143 -12
  155. package/src/queries.ts +70 -4
  156. package/src/test/blob_kzg_warmup.ts +65 -0
  157. package/src/test/chain_monitor.ts +148 -3
  158. package/src/test/eth_cheat_codes.ts +63 -47
  159. package/src/test/index.ts +1 -2
  160. package/src/test/recording_rpc_server.ts +69 -0
  161. package/src/test/rollup_cheat_codes.ts +115 -6
  162. package/src/test/start_anvil.ts +212 -32
  163. package/src/test/upgrade_utils.ts +2 -2
  164. package/src/utils.ts +56 -29
  165. package/dest/contracts/empire_slashing_proposer.d.ts +0 -67
  166. package/dest/contracts/empire_slashing_proposer.d.ts.map +0 -1
  167. package/dest/contracts/empire_slashing_proposer.js +0 -207
  168. package/dest/contracts/tally_slashing_proposer.d.ts +0 -140
  169. package/dest/contracts/tally_slashing_proposer.d.ts.map +0 -1
  170. package/dest/contracts/tally_slashing_proposer.js +0 -320
  171. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +0 -26
  172. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +0 -1
  173. package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +0 -26
  174. package/dest/test/delayed_tx_utils.d.ts +0 -13
  175. package/dest/test/delayed_tx_utils.d.ts.map +0 -1
  176. package/dest/test/delayed_tx_utils.js +0 -28
  177. package/dest/test/tx_delayer.d.ts +0 -36
  178. package/dest/test/tx_delayer.d.ts.map +0 -1
  179. package/src/contracts/empire_slashing_proposer.ts +0 -259
  180. package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +0 -77
  181. package/src/test/delayed_tx_utils.ts +0 -52
@@ -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,11 +19,11 @@ 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
- import type { L1TxUtilsConfig } from './l1_tx_utils/config.js';
27
27
  import type { ExtendedViemWalletClient } from './types.js';
28
28
 
29
29
  const logger = createLogger('ethereum:deploy_aztec_l1_contracts');
@@ -31,9 +31,9 @@ const logger = createLogger('ethereum:deploy_aztec_l1_contracts');
31
31
  const JSON_DEPLOY_RESULT_PREFIX = 'JSON DEPLOY RESULT:';
32
32
 
33
33
  /**
34
- * Runs a process with the given command, arguments, and environment.
35
- * If the process outputs a line starting with JSON_DEPLOY_RESULT_PREFIX,
36
- * the JSON is parsed and returned.
34
+ * Runs a process and parses JSON deploy results from stdout.
35
+ * Lines starting with JSON_DEPLOY_RESULT_PREFIX are parsed and returned.
36
+ * All other stdout goes to logger.info, stderr goes to logger.warn.
37
37
  */
38
38
  function runProcess<T>(
39
39
  command: string,
@@ -49,26 +49,41 @@ function runProcess<T>(
49
49
  });
50
50
 
51
51
  let result: T | undefined;
52
+ let parseError: Error | undefined;
53
+ let settled = false;
52
54
 
53
55
  readline.createInterface({ input: proc.stdout }).on('line', line => {
54
56
  const trimmedLine = line.trim();
55
57
  if (trimmedLine.startsWith(JSON_DEPLOY_RESULT_PREFIX)) {
56
58
  const jsonStr = trimmedLine.slice(JSON_DEPLOY_RESULT_PREFIX.length).trim();
57
- // TODO(AD): should this be a zod parse?
58
- result = JSON.parse(jsonStr);
59
+ try {
60
+ result = JSON.parse(jsonStr);
61
+ } catch {
62
+ parseError = new Error(`Failed to parse deploy result JSON: ${jsonStr.slice(0, 200)}`);
63
+ }
59
64
  } else {
60
65
  logger.info(line);
61
66
  }
62
67
  });
63
- readline.createInterface({ input: proc.stderr }).on('line', logger.error.bind(logger));
68
+ readline.createInterface({ input: proc.stderr }).on('line', logger.warn.bind(logger));
64
69
 
65
70
  proc.on('error', error => {
71
+ if (settled) {
72
+ return;
73
+ }
74
+ settled = true;
66
75
  reject(new Error(`Failed to spawn ${command}: ${error.message}`));
67
76
  });
68
77
 
69
78
  proc.on('close', code => {
79
+ if (settled) {
80
+ return;
81
+ }
82
+ settled = true;
70
83
  if (code !== 0) {
71
- reject(new Error(`${command} exited with code ${code}. See logs for details.\n`));
84
+ reject(new Error(`${command} exited with code ${code}`));
85
+ } else if (parseError) {
86
+ reject(parseError);
72
87
  } else {
73
88
  resolve(result);
74
89
  }
@@ -79,11 +94,16 @@ function runProcess<T>(
79
94
 
80
95
  // Covers an edge where where we may have a cached BlobLib that is not meant for production.
81
96
  // Despite the profile apparently sometimes cached code remains (so says Lasse after his ignition-monorepo arc).
82
- 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
+ ) {
83
103
  if (chainId === mainnet.id) {
84
104
  logger.info(`Recompiling ${script} with production profile for mainnet deployment`);
85
105
  logger.info('This may take a minute but ensures production BlobLib is used.');
86
- await runProcess('forge', ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
106
+ await runProcess(forgeBin, ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
87
107
  }
88
108
  }
89
109
 
@@ -216,7 +236,6 @@ export function computeValidatorData(operator: Operator): ValidatorJson {
216
236
  export interface RollupUpgradeAddresses {
217
237
  rollupAddress: string;
218
238
  verifierAddress: string;
219
- slashFactoryAddress: string;
220
239
  inboxAddress: string;
221
240
  outboxAddress: string;
222
241
  feeJuicePortalAddress: string;
@@ -229,7 +248,6 @@ export interface RollupUpgradeAddresses {
229
248
  export interface ForgeRollupUpgradeResult {
230
249
  rollupAddress: Hex;
231
250
  verifierAddress: Hex;
232
- slashFactoryAddress: Hex;
233
251
  inboxAddress: Hex;
234
252
  outboxAddress: Hex;
235
253
  feeJuicePortalAddress: Hex;
@@ -270,6 +288,7 @@ export async function deployAztecL1Contracts(
270
288
  args: DeployAztecL1ContractsArgs,
271
289
  ): Promise<DeployAztecL1ContractsReturnType> {
272
290
  logger.info(`Deploying L1 contracts with config: ${jsonStringify(args)}`);
291
+ assertValidSlotDurations(args);
273
292
  if (args.initialValidators && args.initialValidators.length > 0 && args.existingTokenAddress) {
274
293
  throw new Error(
275
294
  'Cannot deploy with both initialValidators and existingTokenAddress. ' +
@@ -307,8 +326,9 @@ export async function deployAztecL1Contracts(
307
326
  // Use foundry-artifacts from l1-artifacts package
308
327
  const l1ContractsPath = prepareL1ContractsForDeployment();
309
328
 
329
+ const forgeBin = resolveFoundryBinary('forge');
310
330
  const FORGE_SCRIPT = 'script/deploy/DeployAztecL1Contracts.s.sol';
311
- await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
331
+ await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
312
332
 
313
333
  // Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
314
334
  const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
@@ -321,11 +341,8 @@ export async function deployAztecL1Contracts(
321
341
  );
322
342
  }
323
343
 
324
- // From heuristic testing. More caused issues with anvil.
325
- const MAGIC_ANVIL_BATCH_SIZE = 8;
326
- // Anvil seems to stall with unbounded batch size. Otherwise no max batch size is desirable.
344
+ const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
327
345
  const forgeArgs = [
328
- 'script',
329
346
  FORGE_SCRIPT,
330
347
  '--sig',
331
348
  'run()',
@@ -333,18 +350,22 @@ export async function deployAztecL1Contracts(
333
350
  privateKey,
334
351
  '--rpc-url',
335
352
  rpcUrl,
336
- '--broadcast',
337
- '--batch-size',
338
- MAGIC_ANVIL_BATCH_SIZE.toString(),
339
353
  ...(shouldVerify ? ['--verify'] : []),
340
354
  ];
341
355
  const forgeEnv = {
356
+ // Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
357
+ FORGE_BIN: forgeBin,
342
358
  // Env vars required by l1-contracts/script/deploy/DeploymentConfiguration.sol.
343
359
  NETWORK: getActiveNetworkName(),
344
360
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
345
361
  ...getDeployAztecL1ContractsEnvVars(args),
346
362
  };
347
- const result = await runProcess<ForgeL1ContractsDeployResult>('forge', forgeArgs, forgeEnv, l1ContractsPath);
363
+ const result = await runProcess<ForgeL1ContractsDeployResult>(
364
+ process.execPath,
365
+ [scriptPath, ...forgeArgs],
366
+ forgeEnv,
367
+ l1ContractsPath,
368
+ );
348
369
  if (!result) {
349
370
  throw new Error('Forge script did not output deployment result');
350
371
  }
@@ -390,7 +411,6 @@ export async function deployAztecL1Contracts(
390
411
  governanceProposerAddress: EthAddress.fromString(result.governanceProposerAddress),
391
412
  governanceAddress: EthAddress.fromString(result.governanceAddress),
392
413
  stakingAssetAddress: EthAddress.fromString(result.stakingAssetAddress),
393
- slashFactoryAddress: result.slashFactoryAddress ? EthAddress.fromString(result.slashFactoryAddress) : undefined,
394
414
  feeAssetHandlerAddress: result.feeAssetHandlerAddress
395
415
  ? EthAddress.fromString(result.feeAssetHandlerAddress)
396
416
  : undefined,
@@ -477,7 +497,25 @@ export type VerificationRecord = {
477
497
  libraries: VerificationLibraryEntry[];
478
498
  };
479
499
 
480
- export interface DeployAztecL1ContractsArgs extends Omit<L1ContractsConfig, keyof L1TxUtilsConfig> {
500
+ export interface DeployAztecL1ContractsArgs
501
+ extends Omit<
502
+ L1ContractsConfig,
503
+ | 'gasLimitBufferPercentage'
504
+ | 'maxGwei'
505
+ | 'maxBlobGwei'
506
+ | 'priorityFeeBumpPercentage'
507
+ | 'priorityFeeRetryBumpPercentage'
508
+ | 'minimumPriorityFeePerGas'
509
+ | 'maxSpeedUpAttempts'
510
+ | 'checkIntervalMs'
511
+ | 'stallTimeMs'
512
+ | 'txTimeoutMs'
513
+ | 'cancelTxOnTimeout'
514
+ | 'txCancellationFinalTimeoutMs'
515
+ | 'txUnseenConsideredDroppedMs'
516
+ | 'enableDelayer'
517
+ | 'txDelayerMaxInclusionTimeIntoSlot'
518
+ > {
481
519
  /** The vk tree root. */
482
520
  vkTreeRoot: Fr;
483
521
  /** The hash of the protocol contracts. */
@@ -554,7 +592,7 @@ export function getDeployRollupForUpgradeEnvVars(
554
592
  AZTEC_EXIT_DELAY_SECONDS: args.exitDelaySeconds.toString(),
555
593
  AZTEC_PROVING_COST_PER_MANA: args.provingCostPerMana.toString(),
556
594
  AZTEC_INITIAL_ETH_PER_FEE_ASSET: args.initialEthPerFeeAsset.toString(),
557
- AZTEC_SLASHER_FLAVOR: args.slasherFlavor,
595
+ AZTEC_SLASHER_ENABLED: args.slasherEnabled ? 'true' : 'false',
558
596
  AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS: args.slashingRoundSizeInEpochs.toString(),
559
597
  AZTEC_SLASHING_QUORUM: args.slashingQuorum?.toString(),
560
598
  AZTEC_SLASHING_OFFSET_IN_ROUNDS: args.slashingOffsetInRounds.toString(),
@@ -584,21 +622,15 @@ export const deployRollupForUpgrade = async (
584
622
  // Use foundry-artifacts from l1-artifacts package
585
623
  const l1ContractsPath = prepareL1ContractsForDeployment();
586
624
 
625
+ const forgeBin = resolveFoundryBinary('forge');
587
626
  const FORGE_SCRIPT = 'script/deploy/DeployRollupForUpgrade.s.sol';
588
- await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
627
+ await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
589
628
 
590
- const forgeArgs = [
591
- 'script',
592
- FORGE_SCRIPT,
593
- '--sig',
594
- 'run()',
595
- '--private-key',
596
- privateKey,
597
- '--rpc-url',
598
- rpcUrl,
599
- '--broadcast',
600
- ];
629
+ const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
630
+ const forgeArgs = [FORGE_SCRIPT, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
601
631
  const forgeEnv = {
632
+ // Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
633
+ FORGE_BIN: forgeBin,
602
634
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
603
635
  // Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
604
636
  REGISTRY_ADDRESS: registryAddress.toString(),
@@ -606,7 +638,12 @@ export const deployRollupForUpgrade = async (
606
638
  ...getDeployRollupForUpgradeEnvVars(args),
607
639
  };
608
640
 
609
- const result = await runProcess<ForgeRollupUpgradeResult>('forge', forgeArgs, forgeEnv, l1ContractsPath);
641
+ const result = await runProcess<ForgeRollupUpgradeResult>(
642
+ process.execPath,
643
+ [scriptPath, ...forgeArgs],
644
+ forgeEnv,
645
+ l1ContractsPath,
646
+ );
610
647
  if (!result) {
611
648
  throw new Error('Forge script did not output deployment result');
612
649
  }
@@ -618,6 +655,5 @@ export const deployRollupForUpgrade = async (
618
655
 
619
656
  return {
620
657
  rollup,
621
- slashFactoryAddress: result.slashFactoryAddress,
622
658
  };
623
659
  };
@@ -24,7 +24,7 @@ import {
24
24
  } from './deploy_aztec_l1_contracts.js';
25
25
  import { RegisterNewRollupVersionPayloadArtifact } from './l1_artifacts.js';
26
26
  import { type L1TxUtilsConfig, getL1TxUtilsConfigEnvVars } from './l1_tx_utils/config.js';
27
- import { createL1TxUtilsFromViemWallet } from './l1_tx_utils/factory.js';
27
+ import { createL1TxUtils } from './l1_tx_utils/factory.js';
28
28
  import type { L1TxUtils } from './l1_tx_utils/l1_tx_utils.js';
29
29
  import type { GasPrice, L1TxConfig, L1TxRequest } from './l1_tx_utils/types.js';
30
30
  import type { ExtendedViemWalletClient } from './types.js';
@@ -46,7 +46,7 @@ export class L1Deployer {
46
46
  private createVerificationJson: boolean = false,
47
47
  ) {
48
48
  this.salt = maybeSalt ? padHex(numberToHex(maybeSalt), { size: 32 }) : undefined;
49
- this.l1TxUtils = createL1TxUtilsFromViemWallet(
49
+ this.l1TxUtils = createL1TxUtils(
50
50
  this.client,
51
51
  { logger: this.logger, dateProvider },
52
52
  { ...this.txUtilsConfig, debugMaxGasLimit: acceleratedTestDeployments },
@@ -179,7 +179,7 @@ export async function deployL1Contract(
179
179
 
180
180
  if (!l1TxUtils) {
181
181
  const config = getL1TxUtilsConfigEnvVars();
182
- l1TxUtils = createL1TxUtilsFromViemWallet(
182
+ l1TxUtils = createL1TxUtils(
183
183
  extendedClient,
184
184
  { logger },
185
185
  { ...config, debugMaxGasLimit: acceleratedTestDeployments },