@aztec/ethereum 0.66.0 → 0.67.1-devnet

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.
@@ -1,7 +1,7 @@
1
1
  import { type AztecAddress } from '@aztec/foundation/aztec-address';
2
2
  import { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { type Fr } from '@aztec/foundation/fields';
4
- import { type DebugLogger } from '@aztec/foundation/log';
4
+ import { type Logger } from '@aztec/foundation/log';
5
5
  import {
6
6
  CoinIssuerAbi,
7
7
  CoinIssuerBytecode,
@@ -284,7 +284,7 @@ export const deployL1Contracts = async (
284
284
  rpcUrl: string,
285
285
  account: HDAccount | PrivateKeyAccount,
286
286
  chain: Chain,
287
- logger: DebugLogger,
287
+ logger: Logger,
288
288
  args: DeployL1ContractsArgs,
289
289
  ): Promise<DeployL1Contracts> => {
290
290
  // We are assuming that you are running this on a local anvil node which have 1s block times
@@ -304,10 +304,10 @@ export const deployL1Contracts = async (
304
304
  if (res.error) {
305
305
  throw new Error(`Error setting block interval: ${res.error.message}`);
306
306
  }
307
- logger.info(`Set block interval to ${args.ethereumSlotDuration}`);
307
+ logger.warn(`Set block interval to ${args.ethereumSlotDuration}`);
308
308
  }
309
309
 
310
- logger.info(`Deploying contracts from ${account.address.toString()}...`);
310
+ logger.verbose(`Deploying contracts from ${account.address.toString()}`);
311
311
 
312
312
  const walletClient = createWalletClient({ account, chain, transport: http(rpcUrl) });
313
313
  const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
@@ -315,21 +315,21 @@ export const deployL1Contracts = async (
315
315
  const govDeployer = new L1Deployer(walletClient, publicClient, args.salt, logger);
316
316
 
317
317
  const registryAddress = await govDeployer.deploy(l1Artifacts.registry, [account.address.toString()]);
318
- logger.info(`Deployed Registry at ${registryAddress}`);
318
+ logger.verbose(`Deployed Registry at ${registryAddress}`);
319
319
 
320
320
  const feeAssetAddress = await govDeployer.deploy(l1Artifacts.feeAsset, [
321
321
  'FeeJuice',
322
322
  'FEE',
323
323
  account.address.toString(),
324
324
  ]);
325
- logger.info(`Deployed Fee Juice at ${feeAssetAddress}`);
325
+ logger.verbose(`Deployed Fee Juice at ${feeAssetAddress}`);
326
326
 
327
327
  const stakingAssetAddress = await govDeployer.deploy(l1Artifacts.stakingAsset, [
328
328
  'Staking',
329
329
  'STK',
330
330
  account.address.toString(),
331
331
  ]);
332
- logger.info(`Deployed Staking Asset at ${stakingAssetAddress}`);
332
+ logger.verbose(`Deployed Staking Asset at ${stakingAssetAddress}`);
333
333
 
334
334
  // @todo #8084
335
335
  // @note These numbers are just chosen to make testing simple.
@@ -340,7 +340,7 @@ export const deployL1Contracts = async (
340
340
  quorumSize,
341
341
  roundSize,
342
342
  ]);
343
- logger.info(`Deployed GovernanceProposer at ${governanceProposerAddress}`);
343
+ logger.verbose(`Deployed GovernanceProposer at ${governanceProposerAddress}`);
344
344
 
345
345
  // @note @LHerskind the assets are expected to be the same at some point, but for better
346
346
  // configurability they are different for now.
@@ -348,25 +348,25 @@ export const deployL1Contracts = async (
348
348
  feeAssetAddress.toString(),
349
349
  governanceProposerAddress.toString(),
350
350
  ]);
351
- logger.info(`Deployed Governance at ${governanceAddress}`);
351
+ logger.verbose(`Deployed Governance at ${governanceAddress}`);
352
352
 
353
353
  const coinIssuerAddress = await govDeployer.deploy(l1Artifacts.coinIssuer, [
354
354
  feeAssetAddress.toString(),
355
355
  1n * 10n ** 18n, // @todo #8084
356
356
  governanceAddress.toString(),
357
357
  ]);
358
- logger.info(`Deployed CoinIssuer at ${coinIssuerAddress}`);
358
+ logger.verbose(`Deployed CoinIssuer at ${coinIssuerAddress}`);
359
359
 
360
360
  const rewardDistributorAddress = await govDeployer.deploy(l1Artifacts.rewardDistributor, [
361
361
  feeAssetAddress.toString(),
362
362
  registryAddress.toString(),
363
363
  governanceAddress.toString(),
364
364
  ]);
365
- logger.info(`Deployed RewardDistributor at ${rewardDistributorAddress}`);
365
+ logger.verbose(`Deployed RewardDistributor at ${rewardDistributorAddress}`);
366
366
 
367
367
  logger.verbose(`Waiting for governance contracts to be deployed`);
368
368
  await govDeployer.waitForDeployments();
369
- logger.info(`All governance contracts deployed`);
369
+ logger.verbose(`All governance contracts deployed`);
370
370
 
371
371
  const deployer = new L1Deployer(walletClient, publicClient, args.salt, logger);
372
372
 
@@ -375,7 +375,7 @@ export const deployL1Contracts = async (
375
375
  feeAssetAddress.toString(),
376
376
  args.l2FeeJuiceAddress.toString(),
377
377
  ]);
378
- logger.info(`Deployed Fee Juice Portal at ${feeJuicePortalAddress}`);
378
+ logger.verbose(`Deployed Fee Juice Portal at ${feeJuicePortalAddress}`);
379
379
 
380
380
  const rollupConfigArgs = {
381
381
  aztecSlotDuration: args.aztecSlotDuration,
@@ -394,10 +394,10 @@ export const deployL1Contracts = async (
394
394
  rollupConfigArgs,
395
395
  ];
396
396
  const rollupAddress = await deployer.deploy(l1Artifacts.rollup, rollupArgs);
397
- logger.info(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs);
397
+ logger.verbose(`Deployed Rollup at ${rollupAddress}`, rollupConfigArgs);
398
398
 
399
399
  await deployer.waitForDeployments();
400
- logger.info(`All core contracts deployed`);
400
+ logger.verbose(`All core contracts have been deployed`);
401
401
 
402
402
  const feeJuicePortal = getContract({
403
403
  address: feeJuicePortalAddress.toString(),
@@ -428,13 +428,23 @@ export const deployL1Contracts = async (
428
428
 
429
429
  {
430
430
  const txHash = await feeAsset.write.setFreeForAll([true], {} as any);
431
- logger.info(`Fee asset set to free for all in ${txHash}`);
431
+ logger.verbose(`Fee asset set to free for all in ${txHash}`);
432
432
  txHashes.push(txHash);
433
433
  }
434
434
 
435
- if (args.initialValidators && args.initialValidators.length > 0) {
435
+ const attesters = (await rollup.read
436
+ .getAttesters([])
437
+ .then(attesters =>
438
+ (attesters as `0x${string}`[]).map(attester => EthAddress.fromString(attester.toString())),
439
+ )) as EthAddress[];
440
+
441
+ logger.debug(`Existing attesters`, attesters);
442
+
443
+ const newAttesters = (args.initialValidators ?? []).filter(v => !attesters.some(a => a.equals(v)));
444
+
445
+ if (newAttesters.length > 0) {
436
446
  // Mint tokens, approve them, use cheat code to initialise validator set without setting up the epoch.
437
- const stakeNeeded = MINIMUM_STAKE * BigInt(args.initialValidators.length);
447
+ const stakeNeeded = MINIMUM_STAKE * BigInt(newAttesters.length);
438
448
  await Promise.all(
439
449
  [
440
450
  await stakingAsset.write.mint([walletClient.account.address, stakeNeeded], {} as any),
@@ -442,8 +452,10 @@ export const deployL1Contracts = async (
442
452
  ].map(txHash => publicClient.waitForTransactionReceipt({ hash: txHash })),
443
453
  );
444
454
 
455
+ logger.info(`Minted ${newAttesters.length} validators`);
456
+
445
457
  const initiateValidatorSetTxHash = await rollup.write.cheat__InitialiseValidatorSet([
446
- args.initialValidators.map(v => ({
458
+ newAttesters.map(v => ({
447
459
  attester: v.toString(),
448
460
  proposer: v.toString(),
449
461
  withdrawer: v.toString(),
@@ -451,7 +463,7 @@ export const deployL1Contracts = async (
451
463
  })),
452
464
  ]);
453
465
  txHashes.push(initiateValidatorSetTxHash);
454
- logger.info(`Initialized validator set (${args.initialValidators.join(', ')}) in tx ${initiateValidatorSetTxHash}`);
466
+ logger.info(`Initialized validator set (${newAttesters.join(', ')}) in tx ${initiateValidatorSetTxHash}`);
455
467
  }
456
468
 
457
469
  // @note This value MUST match what is in `constants.nr`. It is currently specified here instead of just importing
@@ -464,7 +476,7 @@ export const deployL1Contracts = async (
464
476
  // @note This is used to ensure we fully wait for the transaction when running against a real chain
465
477
  // otherwise we execute subsequent transactions too soon
466
478
  await publicClient.waitForTransactionReceipt({ hash: mintTxHash });
467
- logger.info(`Funding fee juice portal contract with fee juice in ${mintTxHash}`);
479
+ logger.verbose(`Funding fee juice portal contract with fee juice in ${mintTxHash}`);
468
480
 
469
481
  if (!(await feeJuicePortal.read.initialized([]))) {
470
482
  const initPortalTxHash = await feeJuicePortal.write.initialize([]);
@@ -474,7 +486,7 @@ export const deployL1Contracts = async (
474
486
  logger.verbose(`Fee juice portal is already initialized`);
475
487
  }
476
488
 
477
- logger.info(
489
+ logger.verbose(
478
490
  `Initialized Fee Juice Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeAssetAddress} to L2 ${args.l2FeeJuiceAddress}`,
479
491
  );
480
492
 
@@ -504,15 +516,15 @@ export const deployL1Contracts = async (
504
516
  // Set initial blocks as proven if requested
505
517
  if (args.assumeProvenThrough && args.assumeProvenThrough > 0) {
506
518
  await rollup.write.setAssumeProvenThroughBlockNumber([BigInt(args.assumeProvenThrough)], { account });
507
- logger.info(`Set Rollup assumedProvenUntil to ${args.assumeProvenThrough}`);
519
+ logger.warn(`Rollup set to assumedProvenUntil to ${args.assumeProvenThrough}`);
508
520
  }
509
521
 
510
522
  // Inbox and Outbox are immutable and are deployed from Rollup's constructor so we just fetch them from the contract.
511
523
  const inboxAddress = EthAddress.fromString((await rollup.read.INBOX([])) as any);
512
- logger.info(`Inbox available at ${inboxAddress}`);
524
+ logger.verbose(`Inbox available at ${inboxAddress}`);
513
525
 
514
526
  const outboxAddress = EthAddress.fromString((await rollup.read.OUTBOX([])) as any);
515
- logger.info(`Outbox available at ${outboxAddress}`);
527
+ logger.verbose(`Outbox available at ${outboxAddress}`);
516
528
 
517
529
  // We need to call a function on the registry to set the various contract addresses.
518
530
  const registryContract = getContract({
@@ -521,6 +533,7 @@ export const deployL1Contracts = async (
521
533
  client: walletClient,
522
534
  });
523
535
  if (!(await registryContract.read.isRollupRegistered([getAddress(rollupAddress.toString())]))) {
536
+ logger.info(`Registry ${registryAddress} is not registered to rollup ${rollupAddress}`);
524
537
  const upgradeTxHash = await registryContract.write.upgrade([getAddress(rollupAddress.toString())], { account });
525
538
  logger.verbose(
526
539
  `Upgrading registry contract at ${registryAddress} to rollup ${rollupAddress} in tx ${upgradeTxHash}`,
@@ -562,6 +575,8 @@ export const deployL1Contracts = async (
562
575
  governanceAddress,
563
576
  };
564
577
 
578
+ logger.info(`Aztec L1 contracts initialized`, l1Contracts);
579
+
565
580
  return {
566
581
  walletClient,
567
582
  publicClient,
@@ -576,7 +591,7 @@ class L1Deployer {
576
591
  private walletClient: WalletClient<HttpTransport, Chain, Account>,
577
592
  private publicClient: PublicClient<HttpTransport, Chain>,
578
593
  maybeSalt: number | undefined,
579
- private logger: DebugLogger,
594
+ private logger: Logger,
580
595
  ) {
581
596
  this.salt = maybeSalt ? padHex(numberToHex(maybeSalt), { size: 32 }) : undefined;
582
597
  }
@@ -666,7 +681,7 @@ export async function deployL1Contract(
666
681
  args: readonly unknown[] = [],
667
682
  maybeSalt?: Hex,
668
683
  libraries?: Libraries,
669
- logger?: DebugLogger,
684
+ logger?: Logger,
670
685
  ): Promise<{ address: EthAddress; txHash: Hex | undefined }> {
671
686
  let txHash: Hex | undefined = undefined;
672
687
  let resultingAddress: Hex | null | undefined = undefined;
@@ -1,9 +1,8 @@
1
1
  import { toBigIntBE, toHex } from '@aztec/foundation/bigint-buffer';
2
2
  import { keccak256 } from '@aztec/foundation/crypto';
3
3
  import { type EthAddress } from '@aztec/foundation/eth-address';
4
- import { createDebugLogger } from '@aztec/foundation/log';
4
+ import { createLogger } from '@aztec/foundation/log';
5
5
 
6
- import fs from 'fs';
7
6
  import { type Hex } from 'viem';
8
7
 
9
8
  /**
@@ -18,7 +17,7 @@ export class EthCheatCodes {
18
17
  /**
19
18
  * The logger to use for the eth cheatcodes
20
19
  */
21
- public logger = createDebugLogger('aztec:cheat_codes:eth'),
20
+ public logger = createLogger('ethereum:cheat_codes'),
22
21
  ) {}
23
22
 
24
23
  async rpcCall(method: string, params: any[]) {
@@ -77,11 +76,15 @@ export class EthCheatCodes {
77
76
  * @param numberOfBlocks - The number of blocks to mine
78
77
  */
79
78
  public async mine(numberOfBlocks = 1): Promise<void> {
79
+ await this.doMine(numberOfBlocks);
80
+ this.logger.verbose(`Mined ${numberOfBlocks} L1 blocks`);
81
+ }
82
+
83
+ private async doMine(numberOfBlocks = 1): Promise<void> {
80
84
  const res = await this.rpcCall('hardhat_mine', [numberOfBlocks]);
81
85
  if (res.error) {
82
86
  throw new Error(`Error mining: ${res.error.message}`);
83
87
  }
84
- this.logger.verbose(`Mined ${numberOfBlocks} L1 blocks`);
85
88
  }
86
89
 
87
90
  /**
@@ -188,37 +191,10 @@ export class EthCheatCodes {
188
191
  if (res.error) {
189
192
  throw new Error(`Error warping: ${res.error.message}`);
190
193
  }
191
- await this.mine();
194
+ await this.doMine();
192
195
  this.logger.verbose(`Warped L1 timestamp to ${timestamp}`);
193
196
  }
194
197
 
195
- /**
196
- * Dumps the current chain state to a file.
197
- * @param fileName - The file name to dump state into
198
- */
199
- public async dumpChainState(fileName: string): Promise<void> {
200
- const res = await this.rpcCall('hardhat_dumpState', []);
201
- if (res.error) {
202
- throw new Error(`Error dumping state: ${res.error.message}`);
203
- }
204
- const jsonContent = JSON.stringify(res.result);
205
- fs.writeFileSync(`${fileName}.json`, jsonContent, 'utf8');
206
- this.logger.verbose(`Dumped state to ${fileName}`);
207
- }
208
-
209
- /**
210
- * Loads the chain state from a file.
211
- * @param fileName - The file name to load state from
212
- */
213
- public async loadChainState(fileName: string): Promise<void> {
214
- const data = JSON.parse(fs.readFileSync(`${fileName}.json`, 'utf8'));
215
- const res = await this.rpcCall('hardhat_loadState', [data]);
216
- if (res.error) {
217
- throw new Error(`Error loading state: ${res.error.message}`);
218
- }
219
- this.logger.verbose(`Loaded state from ${fileName}`);
220
- }
221
-
222
198
  /**
223
199
  * Load the value at a storage slot of a contract address on eth
224
200
  * @param contract - The contract address
@@ -1,10 +1,11 @@
1
+ import { times } from '@aztec/foundation/collection';
1
2
  import {
2
3
  type ConfigMappingsType,
3
4
  bigintConfigHelper,
4
5
  getDefaultConfig,
5
6
  numberConfigHelper,
6
7
  } from '@aztec/foundation/config';
7
- import { type DebugLogger } from '@aztec/foundation/log';
8
+ import { type Logger } from '@aztec/foundation/log';
8
9
  import { makeBackoff, retry } from '@aztec/foundation/retry';
9
10
  import { sleep } from '@aztec/foundation/sleep';
10
11
 
@@ -71,6 +72,11 @@ export interface L1TxUtilsConfig {
71
72
  * How long to wait for a tx to be mined before giving up
72
73
  */
73
74
  txTimeoutMs?: number;
75
+ /**
76
+ * How many attempts will be done to get a tx after it was sent?
77
+ * First attempt is done at 1s, second at 2s, third at 3s, etc.
78
+ */
79
+ txPropagationMaxQueryAttempts?: number;
74
80
  }
75
81
 
76
82
  export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
@@ -119,6 +125,11 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
119
125
  env: 'L1_TX_MONITOR_TX_TIMEOUT_MS',
120
126
  ...numberConfigHelper(300_000), // 5 mins
121
127
  },
128
+ txPropagationMaxQueryAttempts: {
129
+ description: 'How many attempts will be done to get a tx after it was sent',
130
+ env: 'L1_TX_PROPAGATION_MAX_QUERY_ATTEMPTS',
131
+ ...numberConfigHelper(3),
132
+ },
122
133
  };
123
134
 
124
135
  export const defaultL1TxUtilsConfig = getDefaultConfig<L1TxUtilsConfig>(l1TxUtilsConfigMappings);
@@ -129,6 +140,12 @@ export interface L1TxRequest {
129
140
  value?: bigint;
130
141
  }
131
142
 
143
+ export interface L1BlobInputs {
144
+ blobs: Uint8Array[];
145
+ kzg: any;
146
+ maxFeePerBlobGas: bigint;
147
+ }
148
+
132
149
  interface GasPrice {
133
150
  maxFeePerGas: bigint;
134
151
  maxPriorityFeePerGas: bigint;
@@ -140,7 +157,7 @@ export class L1TxUtils {
140
157
  constructor(
141
158
  private readonly publicClient: PublicClient,
142
159
  private readonly walletClient: WalletClient<HttpTransport, Chain, Account>,
143
- private readonly logger?: DebugLogger,
160
+ private readonly logger?: Logger,
144
161
  config?: Partial<L1TxUtilsConfig>,
145
162
  ) {
146
163
  this.config = {
@@ -158,6 +175,7 @@ export class L1TxUtils {
158
175
  public async sendTransaction(
159
176
  request: L1TxRequest,
160
177
  _gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint },
178
+ _blobInputs?: L1BlobInputs,
161
179
  ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> {
162
180
  const gasConfig = { ...this.config, ..._gasConfig };
163
181
  const account = this.walletClient.account;
@@ -171,16 +189,20 @@ export class L1TxUtils {
171
189
 
172
190
  const gasPrice = await this.getGasPrice(gasConfig);
173
191
 
192
+ const blobInputs = _blobInputs || {};
174
193
  const txHash = await this.walletClient.sendTransaction({
175
194
  ...request,
195
+ ...blobInputs,
176
196
  gas: gasLimit,
177
197
  maxFeePerGas: gasPrice.maxFeePerGas,
178
198
  maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
179
199
  });
180
200
 
181
- this.logger?.verbose(
182
- `Sent L1 transaction ${txHash} with gas limit ${gasLimit} and price ${formatGwei(gasPrice.maxFeePerGas)} gwei`,
183
- );
201
+ this.logger?.verbose(`Sent L1 transaction ${txHash}`, {
202
+ gasLimit,
203
+ maxFeePerGas: formatGwei(gasPrice.maxFeePerGas),
204
+ maxPriorityFeePerGas: formatGwei(gasPrice.maxPriorityFeePerGas),
205
+ });
184
206
 
185
207
  return { txHash, gasLimit, gasPrice };
186
208
  }
@@ -197,15 +219,19 @@ export class L1TxUtils {
197
219
  initialTxHash: Hex,
198
220
  params: { gasLimit: bigint },
199
221
  _gasConfig?: Partial<L1TxUtilsConfig>,
222
+ _blobInputs?: L1BlobInputs,
200
223
  ): Promise<TransactionReceipt> {
201
224
  const gasConfig = { ...this.config, ..._gasConfig };
202
225
  const account = this.walletClient.account;
226
+ const blobInputs = _blobInputs || {};
227
+ const makeGetTransactionBackoff = () =>
228
+ makeBackoff(times(gasConfig.txPropagationMaxQueryAttempts ?? 3, i => i + 1));
203
229
 
204
230
  // Retry a few times, in case the tx is not yet propagated.
205
231
  const tx = await retry<GetTransactionReturnType>(
206
232
  () => this.publicClient.getTransaction({ hash: initialTxHash }),
207
233
  `Getting L1 transaction ${initialTxHash}`,
208
- makeBackoff([1, 2, 3]),
234
+ makeGetTransactionBackoff(),
209
235
  this.logger,
210
236
  true,
211
237
  );
@@ -230,9 +256,9 @@ export class L1TxUtils {
230
256
  try {
231
257
  const receipt = await this.publicClient.getTransactionReceipt({ hash });
232
258
  if (receipt) {
233
- this.logger?.debug(`L1 Transaction ${hash} confirmed`);
259
+ this.logger?.debug(`L1 transaction ${hash} mined`);
234
260
  if (receipt.status === 'reverted') {
235
- this.logger?.error(`L1 Transaction ${hash} reverted`);
261
+ this.logger?.error(`L1 transaction ${hash} reverted`);
236
262
  }
237
263
  return receipt;
238
264
  }
@@ -248,14 +274,14 @@ export class L1TxUtils {
248
274
  const tx = await retry<GetTransactionReturnType>(
249
275
  () => this.publicClient.getTransaction({ hash: currentTxHash }),
250
276
  `Getting L1 transaction ${currentTxHash}`,
251
- makeBackoff([1, 2, 3]),
277
+ makeGetTransactionBackoff(),
252
278
  this.logger,
253
279
  true,
254
280
  );
255
281
  const timePassed = Date.now() - lastAttemptSent;
256
282
 
257
283
  if (tx && timePassed < gasConfig.stallTimeMs!) {
258
- this.logger?.debug(`L1 Transaction ${currentTxHash} pending. Time passed: ${timePassed}ms`);
284
+ this.logger?.debug(`L1 transaction ${currentTxHash} pending. Time passed: ${timePassed}ms.`);
259
285
 
260
286
  // Check timeout before continuing
261
287
  if (gasConfig.txTimeoutMs) {
@@ -280,12 +306,13 @@ export class L1TxUtils {
280
306
  );
281
307
 
282
308
  this.logger?.debug(
283
- `L1 Transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` +
309
+ `L1 transaction ${currentTxHash} appears stuck. Attempting speed-up ${attempts}/${gasConfig.maxAttempts} ` +
284
310
  `with new priority fee ${formatGwei(newGasPrice.maxPriorityFeePerGas)} gwei`,
285
311
  );
286
312
 
287
313
  currentTxHash = await this.walletClient.sendTransaction({
288
314
  ...request,
315
+ ...blobInputs,
289
316
  nonce,
290
317
  gas: params.gasLimit,
291
318
  maxFeePerGas: newGasPrice.maxFeePerGas,
@@ -308,7 +335,7 @@ export class L1TxUtils {
308
335
  txTimedOut = Date.now() - initialTxTime > gasConfig.txTimeoutMs!;
309
336
  }
310
337
  }
311
- throw new Error(`L1 Transaction ${currentTxHash} timed out`);
338
+ throw new Error(`L1 transaction ${currentTxHash} timed out`);
312
339
  }
313
340
 
314
341
  /**
@@ -320,9 +347,10 @@ export class L1TxUtils {
320
347
  public async sendAndMonitorTransaction(
321
348
  request: L1TxRequest,
322
349
  gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint },
350
+ blobInputs?: L1BlobInputs,
323
351
  ): Promise<TransactionReceipt> {
324
- const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig);
325
- return this.monitorTransaction(request, txHash, { gasLimit }, gasConfig);
352
+ const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig, blobInputs);
353
+ return this.monitorTransaction(request, txHash, { gasLimit }, gasConfig, blobInputs);
326
354
  }
327
355
 
328
356
  /**
@@ -377,10 +405,12 @@ export class L1TxUtils {
377
405
  // Ensure priority fee doesn't exceed max fee
378
406
  const maxPriorityFeePerGas = priorityFee > maxFeePerGas ? maxFeePerGas : priorityFee;
379
407
 
380
- this.logger?.debug(
381
- `Gas price calculation (attempt ${attempt}): baseFee=${formatGwei(baseFee)}, ` +
382
- `maxPriorityFee=${formatGwei(maxPriorityFeePerGas)}, maxFee=${formatGwei(maxFeePerGas)}`,
383
- );
408
+ this.logger?.debug(`Computed gas price`, {
409
+ attempt,
410
+ baseFee: formatGwei(baseFee),
411
+ maxFeePerGas: formatGwei(maxFeePerGas),
412
+ maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
413
+ });
384
414
 
385
415
  return { maxFeePerGas, maxPriorityFeePerGas };
386
416
  }
@@ -388,9 +418,23 @@ export class L1TxUtils {
388
418
  /**
389
419
  * Estimates gas and adds buffer
390
420
  */
391
- public async estimateGas(account: Account, request: L1TxRequest, _gasConfig?: L1TxUtilsConfig): Promise<bigint> {
421
+ public async estimateGas(
422
+ account: Account,
423
+ request: L1TxRequest,
424
+ _gasConfig?: L1TxUtilsConfig,
425
+ _blobInputs?: L1BlobInputs,
426
+ ): Promise<bigint> {
392
427
  const gasConfig = { ...this.config, ..._gasConfig };
393
- const initialEstimate = await this.publicClient.estimateGas({ account, ...request });
428
+ let initialEstimate = 0n;
429
+ // Viem does not allow blobs to be sent via public client's estimate gas, so any estimation will fail.
430
+ // Strangely, the only way to get gas and send blobs is prepareTransactionRequest().
431
+ // See: https://github.com/wevm/viem/issues/2075
432
+ if (_blobInputs) {
433
+ initialEstimate = (await this.walletClient.prepareTransactionRequest({ account, ...request, ..._blobInputs }))
434
+ .gas;
435
+ } else {
436
+ initialEstimate = await this.publicClient.estimateGas({ account, ...request });
437
+ }
394
438
 
395
439
  // Add buffer based on either fixed amount or percentage
396
440
  const withBuffer = initialEstimate + (initialEstimate * (gasConfig.gasLimitBufferPercentage ?? 0n)) / 100n;
@@ -0,0 +1,36 @@
1
+ import fs from 'fs';
2
+
3
+ import { EthCheatCodes } from '../eth_cheat_codes.js';
4
+
5
+ /**
6
+ * A class that provides utility functions for interacting with ethereum (L1) dumping/loading state to/from a file.
7
+ * It is separated to avoid importing fs in the main EthCheatCodes class, which might be used in the browser.
8
+ */
9
+ export class EthCheatCodesWithState extends EthCheatCodes {
10
+ /**
11
+ * Dumps the current chain state to a file.
12
+ * @param fileName - The file name to dump state into
13
+ */
14
+ public async dumpChainState(fileName: string): Promise<void> {
15
+ const res = await this.rpcCall('hardhat_dumpState', []);
16
+ if (res.error) {
17
+ throw new Error(`Error dumping state: ${res.error.message}`);
18
+ }
19
+ const jsonContent = JSON.stringify(res.result);
20
+ fs.writeFileSync(`${fileName}.json`, jsonContent, 'utf8');
21
+ this.logger.verbose(`Dumped state to ${fileName}`);
22
+ }
23
+
24
+ /**
25
+ * Loads the chain state from a file.
26
+ * @param fileName - The file name to load state from
27
+ */
28
+ public async loadChainState(fileName: string): Promise<void> {
29
+ const data = JSON.parse(fs.readFileSync(`${fileName}.json`, 'utf8'));
30
+ const res = await this.rpcCall('hardhat_loadState', [data]);
31
+ if (res.error) {
32
+ throw new Error(`Error loading state: ${res.error.message}`);
33
+ }
34
+ this.logger.verbose(`Loaded state from ${fileName}`);
35
+ }
36
+ }
package/src/test/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './start_anvil.js';
2
2
  export * from './tx_delayer.js';
3
+ export * from './eth_cheat_codes_with_state.js';
@@ -1,4 +1,5 @@
1
- import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
1
+ import { omit } from '@aztec/foundation/collection';
2
+ import { type Logger, createLogger } from '@aztec/foundation/log';
2
3
  import { retryUntil } from '@aztec/foundation/retry';
3
4
 
4
5
  import { inspect } from 'util';
@@ -8,11 +9,12 @@ import {
8
9
  type PublicClient,
9
10
  type WalletClient,
10
11
  keccak256,
12
+ parseTransaction,
11
13
  publicActions,
12
14
  walletActions,
13
15
  } from 'viem';
14
16
 
15
- export function waitUntilBlock<T extends Client>(client: T, blockNumber: number | bigint, logger?: DebugLogger) {
17
+ export function waitUntilBlock<T extends Client>(client: T, blockNumber: number | bigint, logger?: Logger) {
16
18
  const publicClient =
17
19
  'getBlockNumber' in client && typeof client.getBlockNumber === 'function'
18
20
  ? (client as unknown as PublicClient)
@@ -30,7 +32,7 @@ export function waitUntilBlock<T extends Client>(client: T, blockNumber: number
30
32
  );
31
33
  }
32
34
 
33
- export function waitUntilL1Timestamp<T extends Client>(client: T, timestamp: number | bigint, logger?: DebugLogger) {
35
+ export function waitUntilL1Timestamp<T extends Client>(client: T, timestamp: number | bigint, logger?: Logger) {
34
36
  const publicClient =
35
37
  'getBlockNumber' in client && typeof client.getBlockNumber === 'function'
36
38
  ? (client as unknown as PublicClient)
@@ -89,12 +91,13 @@ class DelayerImpl implements Delayer {
89
91
  /**
90
92
  * Returns a new client (without modifying the one passed in) with an injected tx delayer.
91
93
  * The delayer can be used to hold off the next tx to be sent until a given block number.
94
+ * TODO(#10824): This doesn't play along well with blob txs for some reason.
92
95
  */
93
96
  export function withDelayer<T extends WalletClient>(
94
97
  client: T,
95
98
  opts: { ethereumSlotDuration: bigint | number },
96
99
  ): { client: T; delayer: Delayer } {
97
- const logger = createDebugLogger('aztec:ethereum:tx_delayer');
100
+ const logger = createLogger('ethereum:tx_delayer');
98
101
  const delayer = new DelayerImpl(opts);
99
102
  const extended = client
100
103
  // Tweak sendRawTransaction so it uses the delay defined in the delayer.
@@ -116,16 +119,25 @@ export function withDelayer<T extends WalletClient>(
116
119
  // Compute the tx hash manually so we emulate sendRawTransaction response
117
120
  const { serializedTransaction } = args[0];
118
121
  const txHash = keccak256(serializedTransaction);
119
- logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`);
122
+ logger.info(`Delaying tx ${txHash} until ${inspect(waitUntil)}`, {
123
+ argsLen: args.length,
124
+ ...omit(parseTransaction(serializedTransaction), 'data', 'sidecars'),
125
+ });
120
126
 
121
127
  // Do not await here so we can return the tx hash immediately as if it had been sent on the spot.
122
128
  // Instead, delay it so it lands on the desired block number or timestamp, assuming anvil will
123
129
  // mine it immediately.
124
130
  void wait
125
131
  .then(async () => {
126
- const txHash = await client.sendRawTransaction(...args);
127
- logger.info(`Sent previously delayed tx ${txHash} to land on ${inspect(waitUntil)}`);
128
- delayer.txs.push(txHash);
132
+ const clientTxHash = await client.sendRawTransaction(...args);
133
+ if (clientTxHash !== txHash) {
134
+ logger.error(`Tx hash returned by the client does not match computed one`, {
135
+ clientTxHash,
136
+ computedTxHash: txHash,
137
+ });
138
+ }
139
+ logger.info(`Sent previously delayed tx ${clientTxHash} to land on ${inspect(waitUntil)}`);
140
+ delayer.txs.push(clientTxHash);
129
141
  })
130
142
  .catch(err => logger.error(`Error sending tx after delay`, err));
131
143
 
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Fr } from '@aztec/foundation/fields';
2
- import { type DebugLogger } from '@aztec/foundation/log';
2
+ import { type Logger } from '@aztec/foundation/log';
3
3
 
4
4
  import {
5
5
  type Abi,
@@ -27,7 +27,7 @@ export function extractEvent<
27
27
  abi: TAbi,
28
28
  eventName: TEventName,
29
29
  filter?: (log: TEventType) => boolean,
30
- logger?: DebugLogger,
30
+ logger?: Logger,
31
31
  ): TEventType {
32
32
  const event = tryExtractEvent(logs, address, abi, eventName, filter, logger);
33
33
  if (!event) {
@@ -46,7 +46,7 @@ function tryExtractEvent<
46
46
  abi: TAbi,
47
47
  eventName: TEventName,
48
48
  filter?: (log: TEventType) => boolean,
49
- logger?: DebugLogger,
49
+ logger?: Logger,
50
50
  ): TEventType | undefined {
51
51
  for (const log of logs) {
52
52
  if (log.address.toLowerCase() === address.toLowerCase()) {