@aztec/ethereum 0.0.1-commit.f504929 → 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 (144) 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 +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 +127 -12
  31. package/dest/contracts/multicall.d.ts.map +1 -1
  32. package/dest/contracts/multicall.js +201 -94
  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 +5433 -30
  37. package/dest/contracts/rollup.d.ts.map +1 -1
  38. package/dest/contracts/rollup.js +436 -102
  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 +5 -5
  60. package/dest/l1_reader.d.ts.map +1 -1
  61. package/dest/l1_reader.js +9 -7
  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 +57 -23
  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 +34 -8
  73. package/dest/publisher_manager.d.ts.map +1 -1
  74. package/dest/publisher_manager.js +119 -8
  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 +55 -4
  82. package/dest/test/chain_monitor.d.ts.map +1 -1
  83. package/dest/test/chain_monitor.js +104 -3
  84. package/dest/test/eth_cheat_codes.d.ts +31 -6
  85. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  86. package/dest/test/eth_cheat_codes.js +67 -44
  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 +15 -1
  97. package/dest/test/start_anvil.d.ts.map +1 -1
  98. package/dest/test/start_anvil.js +58 -7
  99. package/dest/utils.d.ts +9 -2
  100. package/dest/utils.d.ts.map +1 -1
  101. package/dest/utils.js +59 -24
  102. package/package.json +6 -6
  103. package/src/client.ts +72 -2
  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 +243 -105
  114. package/src/contracts/outbox.ts +42 -8
  115. package/src/contracts/rollup.ts +487 -103
  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 +17 -10
  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 +57 -17
  127. package/src/l1_tx_utils/tx_delayer.ts +16 -7
  128. package/src/publisher_manager.ts +143 -12
  129. package/src/queries.ts +70 -4
  130. package/src/test/blob_kzg_warmup.ts +65 -0
  131. package/src/test/chain_monitor.ts +148 -3
  132. package/src/test/eth_cheat_codes.ts +63 -47
  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 +71 -8
  137. package/src/utils.ts +56 -29
  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,6 +3,7 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
4
  import { makeBackoff, retry } from '@aztec/foundation/retry';
5
5
  import { DateProvider } from '@aztec/foundation/timer';
6
+ import { getErrorCause } from '@aztec/foundation/types';
6
7
  import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
7
8
 
8
9
  import pickBy from 'lodash.pickby';
@@ -16,6 +17,7 @@ import {
16
17
  type Hex,
17
18
  MethodNotFoundRpcError,
18
19
  MethodNotSupportedRpcError,
20
+ RpcRequestError,
19
21
  type StateOverride,
20
22
  decodeErrorResult,
21
23
  formatGwei,
@@ -23,6 +25,7 @@ import {
23
25
  hexToBytes,
24
26
  } from 'viem';
25
27
 
28
+ import { getL1RpcErrorCode } from '../client.js';
26
29
  import type { ViemClient } from '../types.js';
27
30
  import { type L1TxUtilsConfig, defaultL1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
28
31
  import {
@@ -39,6 +42,23 @@ import { getCalldataGasUsage, tryGetCustomErrorNameContractFunction } from './ut
39
42
  // Change this to the current strategy we want to use
40
43
  const CurrentStrategy: PriorityFeeStrategy = P75AllTxsPriorityFeeStrategy;
41
44
 
45
+ /** Error code returned by eth_simulateV1 for `insufficient funds for gas * price + value`. */
46
+ const INSUFFICIENT_FUNDS_RPC_ERROR_CODE = -38014;
47
+
48
+ /**
49
+ * Returns true when an RPC transport error reports the node's upfront funds check
50
+ * (sender balance >= gasLimit * maxFeePerGas) rejecting the request. Matches on the message as well as on the
51
+ * error code, since RPC gateways differ in how they wrap node errors. Apply this only to transport errors: an
52
+ * execution revert string can mention insufficient funds without being this kind of rejection.
53
+ */
54
+ function isInsufficientFundsRpcError(err: unknown): boolean {
55
+ if (getL1RpcErrorCode(err) === INSUFFICIENT_FUNDS_RPC_ERROR_CODE) {
56
+ return true;
57
+ }
58
+ const messages = [err, getErrorCause(err, RpcRequestError)].map(e => (e instanceof Error ? e.message : '')).join(' ');
59
+ return /insufficient funds/i.test(messages);
60
+ }
61
+
42
62
  export class ReadOnlyL1TxUtils {
43
63
  public config: Required<L1TxUtilsConfig>;
44
64
  protected interrupted = false;
@@ -130,9 +150,10 @@ export class ReadOnlyL1TxUtils {
130
150
  const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS);
131
151
  for (let i = 0; i < numBlocks; i++) {
132
152
  // each block can go up 12.5% from previous baseFee
133
- maxFeePerGas = (maxFeePerGas * (1_000n + 125n)) / 1_000n;
153
+ // ceil, (a+b-1)/b, to avoid truncation at small values (e.g. 1 wei blob base fee)
154
+ maxFeePerGas = (maxFeePerGas * (1_000n + 125n) + 999n) / 1_000n;
134
155
  // same for blob gas fee
135
- maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n;
156
+ maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n) + 999n) / 1_000n;
136
157
  }
137
158
 
138
159
  if (attempt > 0) {
@@ -242,13 +263,16 @@ export class ReadOnlyL1TxUtils {
242
263
  const gasConfig = { ...this.config, ..._gasConfig };
243
264
  let initialEstimate = 0n;
244
265
  if (_blobInputs) {
245
- // @note requests with blobs also require maxFeePerBlobGas to be set
266
+ // @note requests with blobs also require maxFeePerBlobGas to be set.
267
+ // Use 2x buffer for maxFeePerBlobGas to avoid stale fees and to pass EIP-4844 validation (even if it is a gas estimation call).
268
+ // 1. maxFeePerBlobGas >= blobBaseFee
269
+ // 2. account balance >= gas * maxFeePerGas + maxFeePerBlobGas * blobCount + value
246
270
  const gasPrice = await this.getGasPrice(gasConfig, true, 0);
247
271
  initialEstimate = await this.client.estimateGas({
248
272
  account,
249
273
  ...request,
250
274
  ..._blobInputs,
251
- maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!,
275
+ maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! * 2n,
252
276
  gas: MAX_L1_TX_LIMIT,
253
277
  blockTag: 'latest',
254
278
  });
@@ -368,8 +392,8 @@ export class ReadOnlyL1TxUtils {
368
392
  gasConfig: L1TxUtilsConfig & { fallbackGasEstimate?: bigint },
369
393
  abi: Abi,
370
394
  ) {
371
- try {
372
- const result = await this.client.simulateBlocks({
395
+ const simulateBlocks = () =>
396
+ this.client.simulateBlocks({
373
397
  validation: false,
374
398
  blocks: [
375
399
  {
@@ -380,18 +404,13 @@ export class ReadOnlyL1TxUtils {
380
404
  ],
381
405
  });
382
406
 
383
- if (result[0].calls[0].status === 'failure') {
384
- this.logger?.error('L1 transaction simulation failed', result[0].calls[0].error);
385
- const decodedError = decodeErrorResult({ abi, data: result[0].calls[0].data });
386
-
387
- throw new Error(
388
- `L1 transaction simulation failed with error ${decodedError.errorName}(${decodedError.args?.join(',')})`,
389
- );
390
- }
391
- this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
392
- return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
407
+ // Only the request itself is wrapped, so that transport-level rejections are never confused with an
408
+ // execution revert (whose decoded revert string could well mention insufficient funds too).
409
+ let result: Awaited<ReturnType<typeof simulateBlocks>>;
410
+ try {
411
+ result = await simulateBlocks();
393
412
  } catch (err) {
394
- if (err instanceof MethodNotFoundRpcError || err instanceof MethodNotSupportedRpcError) {
413
+ if (getErrorCause(err, MethodNotFoundRpcError) || getErrorCause(err, MethodNotSupportedRpcError)) {
395
414
  if (gasConfig.fallbackGasEstimate) {
396
415
  this.logger?.warn(
397
416
  `Node does not support eth_simulateV1 API. Using fallback gas estimate: ${gasConfig.fallbackGasEstimate}`,
@@ -400,8 +419,29 @@ export class ReadOnlyL1TxUtils {
400
419
  }
401
420
  this.logger?.error('Node does not support eth_simulateV1 API');
402
421
  }
422
+ if (isInsufficientFundsRpcError(err)) {
423
+ const rpcError = getErrorCause(err, RpcRequestError);
424
+ const providerMessage = rpcError?.details ?? (err instanceof Error ? err.message : String(err));
425
+ const gas = call?.gas !== undefined ? `, gas ${call.gas}` : '';
426
+ throw new Error(
427
+ `L1 node rejected the eth_simulateV1 request with an insufficient funds error before executing it ` +
428
+ `(sender ${call?.from ?? 'unknown'}${gas}): ${providerMessage}`,
429
+ { cause: err },
430
+ );
431
+ }
403
432
  throw err;
404
433
  }
434
+
435
+ if (result[0].calls[0].status === 'failure') {
436
+ this.logger?.error('L1 transaction simulation failed', result[0].calls[0].error);
437
+ const decodedError = decodeErrorResult({ abi, data: result[0].calls[0].data });
438
+
439
+ throw new Error(
440
+ `L1 transaction simulation failed with error ${decodedError.errorName}(${decodedError.args?.join(',')})`,
441
+ );
442
+ }
443
+ this.logger?.debug(`L1 transaction simulation succeeded`, { ...result[0].calls[0] });
444
+ return { gasUsed: result[0].gasUsed, result: result[0].calls[0].data as `0x${string}` };
405
445
  }
406
446
 
407
447
  public bumpGasLimit(gasLimit: bigint, _gasConfig?: L1TxUtilsConfig): bigint {
@@ -81,7 +81,11 @@ export class Delayer {
81
81
 
82
82
  public maxInclusionTimeIntoSlot: number | undefined = undefined;
83
83
  public ethereumSlotDuration: bigint;
84
- public nextWait: { l1Timestamp: bigint } | { l1BlockNumber: bigint } | { indefinitely: true } | undefined = undefined;
84
+ public nextWait:
85
+ | { l1Timestamp: bigint; timeoutSeconds?: number }
86
+ | { l1BlockNumber: bigint; timeoutSeconds?: number }
87
+ | { indefinitely: true }
88
+ | undefined = undefined;
85
89
  public sentTxHashes: Hex[] = [];
86
90
  public cancelledTxs: Hex[] = [];
87
91
 
@@ -110,13 +114,13 @@ export class Delayer {
110
114
  }
111
115
 
112
116
  /** Delays the next tx to be sent so it lands on the given L1 block number. */
113
- pauseNextTxUntilBlock(l1BlockNumber: number | bigint) {
114
- this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber) };
117
+ pauseNextTxUntilBlock(l1BlockNumber: number | bigint, timeoutSeconds?: number) {
118
+ this.nextWait = { l1BlockNumber: BigInt(l1BlockNumber), timeoutSeconds };
115
119
  }
116
120
 
117
121
  /** Delays the next tx to be sent so it lands on the given timestamp. */
118
- pauseNextTxUntilTimestamp(l1Timestamp: number | bigint) {
119
- this.nextWait = { l1Timestamp: BigInt(l1Timestamp) };
122
+ pauseNextTxUntilTimestamp(l1Timestamp: number | bigint, timeoutSeconds?: number) {
123
+ this.nextWait = { l1Timestamp: BigInt(l1Timestamp), timeoutSeconds };
120
124
  }
121
125
 
122
126
  /** Delays the next tx to be sent indefinitely. */
@@ -198,9 +202,14 @@ export function wrapClientWithDelayer<T extends ViemClient>(client: T, delayer:
198
202
  // Or wait until the desired block number or timestamp
199
203
  wait =
200
204
  'l1BlockNumber' in waitUntil
201
- ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger)
205
+ ? waitUntilBlock(publicClient, waitUntil.l1BlockNumber - 1n, logger, waitUntil.timeoutSeconds)
202
206
  : 'l1Timestamp' in waitUntil
203
- ? waitUntilL1Timestamp(publicClient, waitUntil.l1Timestamp - delayer.ethereumSlotDuration, logger)
207
+ ? waitUntilL1Timestamp(
208
+ publicClient,
209
+ waitUntil.l1Timestamp - delayer.ethereumSlotDuration,
210
+ logger,
211
+ waitUntil.timeoutSeconds,
212
+ )
204
213
  : undefined;
205
214
 
206
215
  logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`, {
@@ -1,6 +1,8 @@
1
1
  import { pick } from '@aztec/foundation/collection';
2
2
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
3
+ import { RunningPromise } from '@aztec/foundation/running-promise';
3
4
 
5
+ import { Multicall3 } from './contracts/multicall.js';
4
6
  import { L1TxUtils, TxUtilsState } from './l1_tx_utils/index.js';
5
7
 
6
8
  // Defines the order in which we prioritise publishers based on their state (first is better)
@@ -27,24 +29,99 @@ const busyStates: TxUtilsState[] = [
27
29
 
28
30
  export type PublisherFilter<UtilsType extends L1TxUtils> = (utils: UtilsType) => boolean;
29
31
 
32
+ /** Config accepted by PublisherManager. */
33
+ type PublisherManagerConfig = {
34
+ publisherAllowInvalidStates?: boolean;
35
+ publisherFundingThreshold?: bigint;
36
+ publisherFundingAmount?: bigint;
37
+ };
38
+
30
39
  export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
31
40
  private log: Logger;
32
- private config: { publisherAllowInvalidStates?: boolean };
41
+ private config: PublisherManagerConfig;
42
+ private static readonly FUNDING_CHECK_INTERVAL_MS = 2 * 60 * 1000;
43
+ protected funder?: UtilsType;
44
+ protected readonly fundingPromise?: RunningPromise;
45
+ private started = false;
33
46
 
34
47
  constructor(
35
- private publishers: UtilsType[],
36
- config: { publisherAllowInvalidStates?: boolean },
37
- bindings?: LoggerBindings,
48
+ protected publishers: UtilsType[],
49
+ config: PublisherManagerConfig,
50
+ opts?: { bindings?: LoggerBindings; funder?: UtilsType },
38
51
  ) {
39
- this.log = createLogger('publisher:manager', bindings);
52
+ this.funder = opts?.funder;
53
+ this.log = createLogger('publisher:manager', opts?.bindings);
40
54
  this.log.info(`PublisherManager initialized with ${publishers.length} publishers.`);
41
55
  this.publishers = publishers;
42
- this.config = pick(config, 'publisherAllowInvalidStates');
56
+ this.config = pick(config, 'publisherAllowInvalidStates', 'publisherFundingThreshold', 'publisherFundingAmount');
57
+
58
+ const hasThreshold = this.config.publisherFundingThreshold !== undefined;
59
+ const hasAmount = this.config.publisherFundingAmount !== undefined;
60
+ if (hasThreshold !== hasAmount) {
61
+ this.log.warn(`Incomplete funding config: both publisherFundingThreshold and publisherFundingAmount must be set`);
62
+ }
63
+
64
+ if (this.funder) {
65
+ const funderAddress = this.funder.getSenderAddress();
66
+ if (publishers.some(p => p.getSenderAddress().equals(funderAddress))) {
67
+ this.log.error(`Funding account ${funderAddress} is also a publisher, disabling funding to avoid self-funding`);
68
+ this.funder = undefined;
69
+ }
70
+ }
71
+
72
+ if (this.funder && hasThreshold && hasAmount) {
73
+ this.fundingPromise = new RunningPromise(
74
+ () => this.triggerFundingIfNeeded(),
75
+ this.log,
76
+ PublisherManager.FUNDING_CHECK_INTERVAL_MS,
77
+ );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Clears any interrupted flag left by a previous {@link stop} so publishing works again after a restart,
83
+ * loads the state of all publishers and the funder, and starts periodic funding checks. Idempotent: a
84
+ * start while already started is a no-op, so it never re-runs `loadStateAndResumeMonitoring` (which
85
+ * would spawn a duplicate background monitor per pending nonce). Lifecycle calls are expected to be
86
+ * serialized by the caller.
87
+ */
88
+ public async start(): Promise<void> {
89
+ if (this.started) {
90
+ this.log.debug('PublisherManager already started, ignoring start');
91
+ return;
92
+ }
93
+
94
+ // Clear the interrupted flag set by a previous stop() so a restarted manager can publish again.
95
+ // On a first start this is a no-op (the flag is already clear).
96
+ this.publishers.forEach(pub => pub.restart());
97
+ this.funder?.restart();
98
+
99
+ await Promise.all([
100
+ ...this.publishers.map(pub => pub.loadStateAndResumeMonitoring()),
101
+ this.funder?.loadStateAndResumeMonitoring(),
102
+ ]);
103
+
104
+ this.fundingPromise?.start();
105
+ // Marked started only once fully up, so a start that failed to load state can be retried.
106
+ this.started = true;
43
107
  }
44
108
 
45
- /** Loads the state of all publishers and resumes monitoring any pending txs */
46
- public async loadState(): Promise<void> {
47
- await Promise.all(this.publishers.map(pub => pub.loadStateAndResumeMonitoring()));
109
+ /**
110
+ * Stops the funding loop, interrupts all publishers so no further L1 txs are sent, and waits (bounded)
111
+ * for their in-flight tx monitor loops to wind down. Idempotent, and the manager may be restarted
112
+ * afterwards via {@link start}, which clears the interrupted flag.
113
+ */
114
+ public async stop(): Promise<void> {
115
+ this.started = false;
116
+ await this.fundingPromise?.stop();
117
+ this.publishers.forEach(pub => pub.interrupt());
118
+ this.funder?.interrupt();
119
+ // Wait for in-flight tx monitor loops to observe the interrupt, so no L1 requests are still
120
+ // being issued after shutdown (e.g. against an anvil instance a test is about to tear down).
121
+ await Promise.all([
122
+ ...this.publishers.map(pub => pub.waitMonitoringStopped()),
123
+ this.funder?.waitMonitoringStopped(),
124
+ ]);
48
125
  }
49
126
 
50
127
  // Finds and prioritises available publishers based on
@@ -83,8 +160,17 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
83
160
  }),
84
161
  );
85
162
 
163
+ // Discount unfunded publishers: a publisher with no ETH cannot send a tx, so it must never win
164
+ // selection regardless of how favourable its state or last-used time is. Fall back to the full
165
+ // set only if every candidate is unfunded, so behaviour is no worse than before.
166
+ let fundedPublishers = publishersWithBalance.filter(p => p.balance > 0n);
167
+ if (fundedPublishers.length === 0) {
168
+ this.log.warn(`All candidate publishers have zero balance; selecting from unfunded publishers.`);
169
+ fundedPublishers = publishersWithBalance;
170
+ }
171
+
86
172
  // Sort based on state, then balance, then time since last use
87
- const sortedPublishers = publishersWithBalance.sort((a, b) => {
173
+ const sortedPublishers = fundedPublishers.sort((a, b) => {
88
174
  const stateComparison = sortOrder.indexOf(a.publisher.state) - sortOrder.indexOf(b.publisher.state);
89
175
  if (stateComparison !== 0) {
90
176
  return stateComparison;
@@ -102,7 +188,52 @@ export class PublisherManager<UtilsType extends L1TxUtils = L1TxUtils> {
102
188
  return sortedPublishers[0].publisher;
103
189
  }
104
190
 
105
- public interrupt() {
106
- this.publishers.forEach(pub => pub.interrupt());
191
+ /** Check all publisher balances and fund those below threshold. */
192
+ private async triggerFundingIfNeeded(): Promise<void> {
193
+ const { funder, config } = this;
194
+ if (!funder || config.publisherFundingThreshold === undefined || config.publisherFundingAmount === undefined) {
195
+ return;
196
+ }
197
+
198
+ const allBalances = await Promise.all(
199
+ this.publishers.map(async pub => ({ balance: await pub.getSenderBalance(), publisher: pub })),
200
+ );
201
+ const lowBalance = allBalances.filter(p => p.balance < config.publisherFundingThreshold!);
202
+ if (lowBalance.length === 0) {
203
+ return;
204
+ }
205
+
206
+ const fundingAmount = config.publisherFundingAmount!;
207
+ const funderBalance = await funder.getSenderBalance();
208
+
209
+ if (funderBalance < 10n * fundingAmount) {
210
+ this.log.warn(`Funding account balance is low`, { funderBalance, threshold: 10n * fundingAmount });
211
+ }
212
+ const affordableCount = Number(funderBalance / fundingAmount);
213
+ if (affordableCount === 0) {
214
+ this.log.error(`Funding account balance too low to fund any publisher`, { funderBalance, fundingAmount });
215
+ return;
216
+ }
217
+ if (affordableCount < lowBalance.length) {
218
+ this.log.warn(`Funder can only afford ${affordableCount}/${lowBalance.length} publishers`, {
219
+ funderBalance,
220
+ fundingAmount,
221
+ });
222
+ }
223
+
224
+ const toFund = lowBalance.slice(0, affordableCount).map(p => p.publisher);
225
+ await this.fundPublishers(toFund);
226
+ }
227
+
228
+ /** Fund publishers via a single Multicall3 aggregate3Value transaction. */
229
+ private async fundPublishers(publishers: UtilsType[]): Promise<void> {
230
+ const fundingAmount = this.config.publisherFundingAmount!;
231
+ const calls = publishers.map(pub => ({
232
+ to: pub.getSenderAddress().toString(),
233
+ value: fundingAmount,
234
+ }));
235
+
236
+ await Multicall3.forwardValue(calls, this.funder!, this.log);
237
+ this.log.info(`Funded ${publishers.length} publishers`);
107
238
  }
108
239
  }
package/src/queries.ts CHANGED
@@ -1,11 +1,77 @@
1
1
  import { EthAddress } from '@aztec/foundation/eth-address';
2
2
 
3
+ import { BaseError, type Block } from 'viem';
4
+
3
5
  import { DefaultL1ContractsConfig, type L1ContractsConfig } from './config.js';
4
6
  import { ReadOnlyGovernanceContract } from './contracts/governance.js';
5
7
  import { GovernanceProposerContract } from './contracts/governance_proposer.js';
6
8
  import { InboxContract } from './contracts/inbox.js';
7
9
  import { RollupContract } from './contracts/rollup.js';
8
- import type { ViemPublicClient } from './types.js';
10
+ import type { ViemClient, ViemPublicClient } from './types.js';
11
+
12
+ /**
13
+ * Returns the L1 finalized block, or `undefined` if the chain does not yet have one
14
+ * (common on freshly started devnets). Rethrows any other RPC error.
15
+ */
16
+ export async function getFinalizedL1Block(client: ViemClient): Promise<Block<bigint, false, 'finalized'> | undefined> {
17
+ try {
18
+ return await client.getBlock({ blockTag: 'finalized', includeTransactions: false });
19
+ } catch (err) {
20
+ if (isFinalizedBlockTagNotFoundError(err)) {
21
+ return undefined;
22
+ }
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ // Error messages returned by popular Ethereum execution clients when
28
+ // eth_getBlockByNumber / eth_call is called with blockTag "finalized" or
29
+ // "safe" and no such block has been produced yet:
30
+ //
31
+ // geth "finalized block not found"
32
+ // "safe block not found"
33
+ // reth "block not found: finalized"
34
+ // "block not found: safe"
35
+ // nethermind "Unknown block error" (same for both tags)
36
+ // besu "Unknown block"
37
+ // erigon 'block "finalized" not available (head block: N)'
38
+ // 'block "safe" not available (head block: N)'
39
+ //
40
+ // A combined regex covers all five:
41
+ const FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE =
42
+ /(finalized|safe) block not found|block not found: (finalized|safe)|unknown block|block "(finalized|safe)" not available/i;
43
+
44
+ /**
45
+ * Returns true if the error originates from an RPC call that failed because
46
+ * the "finalized" (or "safe") block tag is not yet available on the chain.
47
+ */
48
+ export function isFinalizedBlockTagNotFoundError(err: unknown): boolean {
49
+ if (!err) {
50
+ return false;
51
+ }
52
+
53
+ if (err instanceof BaseError) {
54
+ const hit = err.walk((e: any) => matchesFinalizedBlockTagNotFound(e));
55
+ if (hit) {
56
+ return true;
57
+ }
58
+ }
59
+
60
+ for (let cur: any = err, i = 0; cur && i < 10; cur = cur.cause, i++) {
61
+ if (matchesFinalizedBlockTagNotFound(cur)) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ function matchesFinalizedBlockTagNotFound(e: any): boolean {
69
+ if (!e) {
70
+ return false;
71
+ }
72
+ const text = `${e.details ?? ''} ${e.message ?? ''} ${e.shortMessage ?? ''}`;
73
+ return FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE.test(text);
74
+ }
9
75
 
10
76
  /** Reads the L1ContractsConfig from L1 contracts. */
11
77
  export async function getL1ContractsConfig(
@@ -76,8 +142,8 @@ export async function getL1ContractsConfig(
76
142
  slasherProposer?.getRoundSize() ?? 0n,
77
143
  slasherProposer?.getLifetimeInRounds() ?? 0n,
78
144
  slasherProposer?.getExecutionDelayInRounds() ?? 0n,
79
- slasherProposer?.type === 'tally' ? slasherProposer.getSlashOffsetInRounds() : 0n,
80
- slasherProposer?.type === 'tally' ? slasherProposer.getSlashingAmounts() : [0n, 0n, 0n],
145
+ slasherProposer?.getSlashOffsetInRounds() ?? 0n,
146
+ slasherProposer?.getSlashingAmounts() ?? [0n, 0n, 0n],
81
147
  slasher?.getVetoer() ?? EthAddress.ZERO,
82
148
  slasher?.getSlashingDisableDuration() ?? 0,
83
149
  rollup.getManaTarget(),
@@ -114,7 +180,7 @@ export async function getL1ContractsConfig(
114
180
  rollupVersion: Number(rollupVersion),
115
181
  genesisArchiveTreeRoot: genesisArchiveTreeRoot.toString(),
116
182
  exitDelaySeconds: Number(exitDelay),
117
- slasherFlavor: slasherProposer?.type ?? 'tally',
183
+ slasherEnabled: !!slasherProposer,
118
184
  slashingOffsetInRounds: Number(slashingOffsetInRounds),
119
185
  slashAmountSmall: slashingAmounts[0],
120
186
  slashAmountMedium: slashingAmounts[1],
@@ -0,0 +1,65 @@
1
+ import { getBytesPerBlob, getBytesPerCommitment, getKzg } from '@aztec/blob-lib';
2
+ import type { Logger } from '@aztec/foundation/log';
3
+
4
+ import type { ExtendedViemWalletClient } from '../types.js';
5
+
6
+ /**
7
+ * Warms both KZG trusted setups in parallel for tests against a local anvil:
8
+ * - anvil's own setup, loaded lazily when it first validates a blob sidecar in `eth_sendRawTransaction`
9
+ * - our `@crate-crypto/node-eth-kzg` singleton (`getKzg()`, a ~2.2s synchronous precomp build)
10
+ *
11
+ * Without this, the first checkpoint publish pays both serially (~4.4s). The recipe does all RPC round
12
+ * trips up front, fires a single bare raw send carrying a fake (garbage) blob sidecar that anvil rejects
13
+ * after loading its trusted setup, yields once to flush the socket write, then synchronously builds our
14
+ * own precomp tables while anvil warms in parallel. The fake tx is rejected at admission, so it never
15
+ * enters the pool, mines no block, and burns no nonce — sending it from the passed client is harmless.
16
+ *
17
+ * Best-effort: any failure is debug-logged and swallowed, leaving lazy init as the fallback. Never throws.
18
+ *
19
+ * @param l1Client - The L1 client whose account sends the (rejected) warm-up blob tx.
20
+ */
21
+ export async function warmBlobKzg(l1Client: ExtendedViemWalletClient, logger?: Logger): Promise<void> {
22
+ try {
23
+ // A pure-JS fake kzg: returns deterministic garbage of the correct lengths (commitment and proof are
24
+ // both 48 bytes). It makes viem build a syntactically valid sidecar without initializing any native
25
+ // module, so anvil loads its trusted setup to validate the (invalid) proof and then rejects the tx.
26
+ const fakeKzg = {
27
+ blobToKzgCommitment: () => {
28
+ const commitment = new Uint8Array(getBytesPerCommitment());
29
+ commitment.fill(0xab);
30
+ commitment[0] = 0xc0;
31
+ return commitment;
32
+ },
33
+ computeBlobKzgProof: () => {
34
+ const proof = new Uint8Array(getBytesPerCommitment());
35
+ proof.fill(0xcd);
36
+ proof[0] = 0xc0;
37
+ return proof;
38
+ },
39
+ };
40
+
41
+ // All RPC round trips (nonce, fees, gas) happen here, so only a single socket write remains to flush.
42
+ const prepared = await l1Client.prepareTransactionRequest({
43
+ blobs: [new Uint8Array(getBytesPerBlob())],
44
+ kzg: fakeKzg,
45
+ to: '0x0000000000000000000000000000000000000000',
46
+ value: 0n,
47
+ maxFeePerBlobGas: 1_000_000_000_000n,
48
+ });
49
+ const serialized = await l1Client.signTransaction(prepared);
50
+
51
+ // Fire the raw send without awaiting and swallow the expected rejection.
52
+ const sent = l1Client.request({ method: 'eth_sendRawTransaction', params: [serialized] }).catch(() => {});
53
+
54
+ // Flush the single socket write so anvil receives the tx and starts loading its trusted setup in its
55
+ // own process before we block the event loop below.
56
+ await new Promise(resolve => setImmediate(resolve));
57
+
58
+ // Synchronously build our precomp tables (~2.2s, event-loop-blocking); anvil warms in parallel.
59
+ getKzg();
60
+
61
+ await sent;
62
+ } catch (err) {
63
+ logger?.debug('Failed to warm blob KZG; falling back to lazy init', { err });
64
+ }
65
+ }