@aztec/sequencer-client 0.71.0 → 0.72.1

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.
@@ -19,6 +19,7 @@ import {
19
19
  import { type FeeRecipient, type RootRollupPublicInputs } from '@aztec/circuits.js/rollup';
20
20
  import {
21
21
  type EthereumChain,
22
+ FormattedViemError,
22
23
  type GasPrice,
23
24
  type L1ContractsConfig,
24
25
  L1TxUtils,
@@ -416,7 +417,7 @@ export class L1Publisher {
416
417
  digest: Buffer.alloc(32),
417
418
  signatures: [],
418
419
  },
419
- ): Promise<void> {
420
+ ): Promise<bigint> {
420
421
  const ts = BigInt((await this.publicClient.getBlock()).timestamp + this.ethereumSlotDuration);
421
422
 
422
423
  const formattedSignatures = attestationData.signatures.map(attest => attest.toViemSignature());
@@ -441,6 +442,7 @@ export class L1Publisher {
441
442
  }
442
443
  throw error;
443
444
  }
445
+ return ts;
444
446
  }
445
447
 
446
448
  public async getCurrentEpochCommittee(): Promise<EthAddress[]> {
@@ -545,8 +547,8 @@ export class L1Publisher {
545
547
  account: this.account,
546
548
  });
547
549
  } catch (err) {
548
- const msg = formatViemError(err);
549
- logger.error(`Failed to vote`, msg);
550
+ const { message, metaMessages } = formatViemError(err);
551
+ logger.error(`Failed to vote`, message, { metaMessages });
550
552
  this.myLastVotes[voteType] = cachedMyLastVote;
551
553
  return false;
552
554
  }
@@ -609,15 +611,15 @@ export class L1Publisher {
609
611
  // This means that we can avoid the simulation issues in later checks.
610
612
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
611
613
  // make time consistency checks break.
612
- await this.validateBlockForSubmission(block.header, {
614
+ const ts = await this.validateBlockForSubmission(block.header, {
613
615
  digest: digest.toBuffer(),
614
616
  signatures: attestations ?? [],
615
617
  });
616
618
 
617
619
  this.log.debug(`Submitting propose transaction`);
618
620
  const result = proofQuote
619
- ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote, opts)
620
- : await this.sendProposeTx(proposeTxArgs, opts);
621
+ ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote, opts, ts)
622
+ : await this.sendProposeTx(proposeTxArgs, opts, ts);
621
623
 
622
624
  if (!result?.receipt) {
623
625
  this.log.info(`Failed to publish block ${block.number} to L1`, ctx);
@@ -689,9 +691,17 @@ export class L1Publisher {
689
691
  }),
690
692
  });
691
693
  } catch (err) {
692
- this.log.error(`Failed to claim epoch proof right`, err, {
693
- proofQuote: proofQuote.toInspect(),
694
- });
694
+ if (err instanceof FormattedViemError) {
695
+ const { message, metaMessages } = err;
696
+ this.log.error(`Failed to claim epoch proof right`, message, {
697
+ metaMessages,
698
+ proofQuote: proofQuote.toInspect(),
699
+ });
700
+ } else {
701
+ this.log.error(`Failed to claim epoch proof right`, err, {
702
+ proofQuote: proofQuote.toInspect(),
703
+ });
704
+ }
695
705
  return false;
696
706
  }
697
707
 
@@ -961,6 +971,8 @@ export class L1Publisher {
961
971
 
962
972
  private async prepareProposeTx(encodedData: L1ProcessArgs) {
963
973
  const kzg = Blob.getViemKzgInstance();
974
+ const blobInput = Blob.getEthBlobEvaluationInputs(encodedData.blobs);
975
+ this.log.debug('Validating blob input', { blobInput });
964
976
  const blobEvaluationGas = await this.l1TxUtils.estimateGas(
965
977
  this.account,
966
978
  {
@@ -968,7 +980,7 @@ export class L1Publisher {
968
980
  data: encodeFunctionData({
969
981
  abi: this.rollupContract.abi,
970
982
  functionName: 'validateBlobs',
971
- args: [Blob.getEthBlobEvaluationInputs(encodedData.blobs)],
983
+ args: [blobInput],
972
984
  }),
973
985
  },
974
986
  {},
@@ -978,12 +990,6 @@ export class L1Publisher {
978
990
  },
979
991
  );
980
992
 
981
- // @note We perform this guesstimate instead of the usual `gasEstimate` since
982
- // viem will use the current state to simulate against, which means that
983
- // we will fail estimation in the case where we are simulating for the
984
- // first ethereum block within our slot (as current time is not in the
985
- // slot yet).
986
- const gasGuesstimate = blobEvaluationGas + L1Publisher.PROPOSE_GAS_GUESS;
987
993
  const attestations = encodedData.attestations
988
994
  ? encodedData.attestations.map(attest => attest.toViemSignature())
989
995
  : [];
@@ -1003,10 +1009,10 @@ export class L1Publisher {
1003
1009
  attestations,
1004
1010
  // TODO(#9101): Extract blobs from beacon chain => calldata will only contain what's needed to verify blob and body input can be removed
1005
1011
  `0x${encodedData.body.toString('hex')}`,
1006
- Blob.getEthBlobEvaluationInputs(encodedData.blobs),
1012
+ blobInput,
1007
1013
  ] as const;
1008
1014
 
1009
- return { args, gas: gasGuesstimate };
1015
+ return { args, blobEvaluationGas };
1010
1016
  }
1011
1017
 
1012
1018
  private getSubmitEpochProofArgs(args: {
@@ -1042,26 +1048,58 @@ export class L1Publisher {
1042
1048
  private async sendProposeTx(
1043
1049
  encodedData: L1ProcessArgs,
1044
1050
  opts: { txTimeoutAt?: Date } = {},
1051
+ timestamp: bigint,
1045
1052
  ): Promise<L1ProcessReturnType | undefined> {
1046
1053
  if (this.interrupted) {
1047
1054
  return undefined;
1048
1055
  }
1049
1056
  try {
1050
1057
  const kzg = Blob.getViemKzgInstance();
1051
- const { args, gas } = await this.prepareProposeTx(encodedData);
1058
+ const { args, blobEvaluationGas } = await this.prepareProposeTx(encodedData);
1052
1059
  const data = encodeFunctionData({
1053
1060
  abi: this.rollupContract.abi,
1054
1061
  functionName: 'propose',
1055
1062
  args,
1056
1063
  });
1064
+
1065
+ const simulationResult = await this.l1TxUtils.simulateGasUsed(
1066
+ {
1067
+ to: this.rollupContract.address,
1068
+ data,
1069
+ gas: L1Publisher.PROPOSE_GAS_GUESS,
1070
+ },
1071
+ {
1072
+ // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1073
+ time: timestamp + 1n,
1074
+ // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit
1075
+ gasLimit: L1Publisher.PROPOSE_GAS_GUESS * 2n,
1076
+ },
1077
+ [
1078
+ {
1079
+ address: this.rollupContract.address,
1080
+ // @note we override checkBlob to false since blobs are not part simulate()
1081
+ stateDiff: [
1082
+ {
1083
+ slot: toHex(9n, true),
1084
+ value: toHex(0n, true),
1085
+ },
1086
+ ],
1087
+ },
1088
+ ],
1089
+ {
1090
+ // @note fallback gas estimate to use if the node doesn't support simulation API
1091
+ fallbackGasEstimate: L1Publisher.PROPOSE_GAS_GUESS,
1092
+ },
1093
+ );
1094
+
1057
1095
  const result = await this.l1TxUtils.sendAndMonitorTransaction(
1058
1096
  {
1059
1097
  to: this.rollupContract.address,
1060
1098
  data,
1061
1099
  },
1062
1100
  {
1063
- fixedGas: gas,
1064
1101
  ...opts,
1102
+ gasLimit: this.l1TxUtils.bumpGasLimit(simulationResult + blobEvaluationGas),
1065
1103
  },
1066
1104
  {
1067
1105
  blobs: encodedData.blobs.map(b => b.dataWithZeros),
@@ -1076,7 +1114,12 @@ export class L1Publisher {
1076
1114
  data,
1077
1115
  };
1078
1116
  } catch (err) {
1079
- this.log.error(`Rollup publish failed.`, err);
1117
+ if (err instanceof FormattedViemError) {
1118
+ const { message, metaMessages } = err;
1119
+ this.log.error(`Rollup publish failed.`, message, { metaMessages });
1120
+ } else {
1121
+ this.log.error(`Rollup publish failed.`, err);
1122
+ }
1080
1123
  return undefined;
1081
1124
  }
1082
1125
  }
@@ -1085,26 +1128,58 @@ export class L1Publisher {
1085
1128
  encodedData: L1ProcessArgs,
1086
1129
  quote: EpochProofQuote,
1087
1130
  opts: { txTimeoutAt?: Date } = {},
1131
+ timestamp: bigint,
1088
1132
  ): Promise<L1ProcessReturnType | undefined> {
1089
1133
  if (this.interrupted) {
1090
1134
  return undefined;
1091
1135
  }
1136
+
1092
1137
  try {
1093
1138
  const kzg = Blob.getViemKzgInstance();
1094
- const { args, gas } = await this.prepareProposeTx(encodedData);
1139
+ const { args, blobEvaluationGas } = await this.prepareProposeTx(encodedData);
1095
1140
  const data = encodeFunctionData({
1096
1141
  abi: this.rollupContract.abi,
1097
1142
  functionName: 'proposeAndClaim',
1098
1143
  args: [...args, quote.toViemArgs()],
1099
1144
  });
1145
+
1146
+ const simulationResult = await this.l1TxUtils.simulateGasUsed(
1147
+ {
1148
+ to: this.rollupContract.address,
1149
+ data,
1150
+ gas: L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS,
1151
+ },
1152
+ {
1153
+ // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1154
+ time: timestamp + 1n,
1155
+ // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit
1156
+ gasLimit: L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS * 2n,
1157
+ },
1158
+ [
1159
+ {
1160
+ address: this.rollupContract.address,
1161
+ // @note we override checkBlob to false since blobs are not part simulate()
1162
+ stateDiff: [
1163
+ {
1164
+ slot: toHex(9n, true),
1165
+ value: toHex(0n, true),
1166
+ },
1167
+ ],
1168
+ },
1169
+ ],
1170
+ {
1171
+ // @note fallback gas estimate to use if the node doesn't support simulation API
1172
+ fallbackGasEstimate: L1Publisher.PROPOSE_AND_CLAIM_GAS_GUESS,
1173
+ },
1174
+ );
1100
1175
  const result = await this.l1TxUtils.sendAndMonitorTransaction(
1101
1176
  {
1102
1177
  to: this.rollupContract.address,
1103
1178
  data,
1104
1179
  },
1105
1180
  {
1106
- fixedGas: gas,
1107
1181
  ...opts,
1182
+ gasLimit: this.l1TxUtils.bumpGasLimit(simulationResult + blobEvaluationGas),
1108
1183
  },
1109
1184
  {
1110
1185
  blobs: encodedData.blobs.map(b => b.dataWithZeros),
@@ -1120,7 +1195,12 @@ export class L1Publisher {
1120
1195
  data,
1121
1196
  };
1122
1197
  } catch (err) {
1123
- this.log.error(`Rollup publish failed.`, err);
1198
+ if (err instanceof FormattedViemError) {
1199
+ const { message, metaMessages } = err;
1200
+ this.log.error(`Rollup publish failed.`, message, { metaMessages });
1201
+ } else {
1202
+ this.log.error(`Rollup publish failed.`, err);
1203
+ }
1124
1204
  return undefined;
1125
1205
  }
1126
1206
  }
@@ -17,13 +17,13 @@ export function getDefaultAllowedSetupFunctions(): AllowedElement[] {
17
17
  {
18
18
  address: ProtocolContractAddress.FeeJuice,
19
19
  // We can't restrict the selector because public functions get routed via dispatch.
20
- // selector: FunctionSelector.fromSignature('_increase_public_balance((Field),Field)'),
20
+ // selector: FunctionSelector.fromSignature('_increase_public_balance((Field),(Field,Field))'),
21
21
  },
22
22
  // needed for private transfers via FPC
23
23
  {
24
24
  classId: getContractClassFromArtifact(TokenContractArtifact).id,
25
25
  // We can't restrict the selector because public functions get routed via dispatch.
26
- // selector: FunctionSelector.fromSignature('_increase_public_balance((Field),Field)'),
26
+ // selector: FunctionSelector.fromSignature('_increase_public_balance((Field),(Field,Field))'),
27
27
  },
28
28
  {
29
29
  classId: getContractClassFromArtifact(FPCContract.artifact).id,
@@ -236,20 +236,15 @@ export class Sequencer {
236
236
  this.setState(SequencerState.PROPOSER_CHECK, 0n);
237
237
 
238
238
  const chainTip = await this.l2BlockSource.getBlock(-1);
239
- const historicalHeader = chainTip?.header;
240
239
 
241
- const newBlockNumber =
242
- (historicalHeader === undefined
243
- ? await this.l2BlockSource.getBlockNumber()
244
- : Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1;
240
+ const newBlockNumber = (chainTip?.header.globalVariables.blockNumber.toNumber() ?? 0) + 1;
245
241
 
246
242
  // If we cannot find a tip archive, assume genesis.
247
- const chainTipArchive =
248
- chainTip == undefined ? new Fr(GENESIS_ARCHIVE_ROOT).toBuffer() : chainTip?.archive.root.toBuffer();
243
+ const chainTipArchive = chainTip?.archive.root ?? new Fr(GENESIS_ARCHIVE_ROOT);
249
244
 
250
245
  let slot: bigint;
251
246
  try {
252
- slot = await this.mayProposeBlock(chainTipArchive, BigInt(newBlockNumber));
247
+ slot = await this.mayProposeBlock(chainTipArchive.toBuffer(), BigInt(newBlockNumber));
253
248
  } catch (err) {
254
249
  this.log.debug(`Cannot propose for block ${newBlockNumber}`);
255
250
  return;
@@ -278,7 +273,7 @@ export class Sequencer {
278
273
 
279
274
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
280
275
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
281
- chainTipArchive: new Fr(chainTipArchive),
276
+ chainTipArchive,
282
277
  blockNumber: newBlockNumber,
283
278
  slot,
284
279
  });
@@ -289,7 +284,7 @@ export class Sequencer {
289
284
 
290
285
  // If I created a "partial" header here that should make our job much easier.
291
286
  const proposalHeader = new BlockHeader(
292
- new AppendOnlyTreeSnapshot(Fr.fromBuffer(chainTipArchive), 1),
287
+ new AppendOnlyTreeSnapshot(chainTipArchive, 1),
293
288
  ContentCommitment.empty(),
294
289
  StateReference.empty(),
295
290
  newGlobalVariables,
@@ -302,9 +297,12 @@ export class Sequencer {
302
297
  // @note It is very important that the following function will FAIL and not just return early
303
298
  // if it have made any state changes. If not, we won't rollback the state, and you will
304
299
  // be in for a world of pain.
305
- await this.buildBlockAndAttemptToPublish(pendingTxs, proposalHeader, historicalHeader);
300
+ await this.buildBlockAndAttemptToPublish(pendingTxs, proposalHeader);
306
301
  } catch (err) {
307
302
  this.log.error(`Error assembling block`, err, { blockNumber: newBlockNumber, slot });
303
+
304
+ // If the block failed to build, we might still want to claim the proving rights
305
+ await this.claimEpochProofRightIfAvailable(slot);
308
306
  }
309
307
  this.setState(SequencerState.IDLE, 0n);
310
308
  }
@@ -378,14 +376,13 @@ export class Sequencer {
378
376
  protected async buildBlock(
379
377
  pendingTxs: Iterable<Tx>,
380
378
  newGlobalVariables: GlobalVariables,
381
- historicalHeader?: BlockHeader,
382
379
  opts: { validateOnly?: boolean } = {},
383
380
  ) {
384
- const blockNumber = newGlobalVariables.blockNumber.toBigInt();
381
+ const blockNumber = newGlobalVariables.blockNumber.toNumber();
385
382
  const slot = newGlobalVariables.slotNumber.toBigInt();
386
383
 
387
384
  this.log.debug(`Requesting L1 to L2 messages from contract for block ${blockNumber}`);
388
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
385
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(BigInt(blockNumber));
389
386
  const msgCount = l1ToL2Messages.length;
390
387
 
391
388
  this.log.verbose(`Building block ${blockNumber} for slot ${slot}`, {
@@ -396,23 +393,21 @@ export class Sequencer {
396
393
  });
397
394
 
398
395
  // Sync to the previous block at least
399
- await this.worldState.syncImmediate(newGlobalVariables.blockNumber.toNumber() - 1);
400
- this.log.debug(`Synced to previous block ${newGlobalVariables.blockNumber.toNumber() - 1}`);
396
+ await this.worldState.syncImmediate(blockNumber - 1);
397
+ this.log.debug(`Synced to previous block ${blockNumber - 1}`);
401
398
 
402
399
  // NB: separating the dbs because both should update the state
403
400
  const publicProcessorFork = await this.worldState.fork();
404
401
  const orchestratorFork = await this.worldState.fork();
405
402
 
403
+ const previousBlockHeader =
404
+ (await this.l2BlockSource.getBlock(blockNumber - 1))?.header ?? orchestratorFork.getInitialHeader();
405
+
406
406
  try {
407
- const processor = this.publicProcessorFactory.create(
408
- publicProcessorFork,
409
- historicalHeader,
410
- newGlobalVariables,
411
- true,
412
- );
407
+ const processor = this.publicProcessorFactory.create(publicProcessorFork, newGlobalVariables, true);
413
408
  const blockBuildingTimer = new Timer();
414
409
  const blockBuilder = this.blockBuilderFactory.create(orchestratorFork);
415
- await blockBuilder.startNewBlock(newGlobalVariables, l1ToL2Messages);
410
+ await blockBuilder.startNewBlock(newGlobalVariables, l1ToL2Messages, previousBlockHeader);
416
411
 
417
412
  // Deadline for processing depends on whether we're proposing a block
418
413
  const secondsIntoSlot = this.getSecondsIntoSlot(slot);
@@ -517,16 +512,11 @@ export class Sequencer {
517
512
  *
518
513
  * @param pendingTxs - Iterable of pending transactions to construct the block from
519
514
  * @param proposalHeader - The partial header constructed for the proposal
520
- * @param historicalHeader - The historical header of the parent
521
515
  */
522
- @trackSpan('Sequencer.buildBlockAndAttemptToPublish', (_validTxs, proposalHeader, _historicalHeader) => ({
516
+ @trackSpan('Sequencer.buildBlockAndAttemptToPublish', (_validTxs, proposalHeader) => ({
523
517
  [Attributes.BLOCK_NUMBER]: proposalHeader.globalVariables.blockNumber.toNumber(),
524
518
  }))
525
- private async buildBlockAndAttemptToPublish(
526
- pendingTxs: Iterable<Tx>,
527
- proposalHeader: BlockHeader,
528
- historicalHeader: BlockHeader | undefined,
529
- ): Promise<void> {
519
+ private async buildBlockAndAttemptToPublish(pendingTxs: Iterable<Tx>, proposalHeader: BlockHeader): Promise<void> {
530
520
  await this.publisher.validateBlockForSubmission(proposalHeader);
531
521
 
532
522
  const newGlobalVariables = proposalHeader.globalVariables;
@@ -541,7 +531,7 @@ export class Sequencer {
541
531
  const proofQuotePromise = this.createProofClaimForPreviousEpoch(slot);
542
532
 
543
533
  try {
544
- const buildBlockRes = await this.buildBlock(pendingTxs, newGlobalVariables, historicalHeader);
534
+ const buildBlockRes = await this.buildBlock(pendingTxs, newGlobalVariables);
545
535
  const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer } = buildBlockRes;
546
536
 
547
537
  // TODO(@PhilWindle) We should probably periodically check for things like another
@@ -640,8 +630,8 @@ export class Sequencer {
640
630
  this.log.debug('Creating block proposal for validators');
641
631
  const proposal = await this.validatorClient.createBlockProposal(block.header, block.archive.root, txHashes);
642
632
  if (!proposal) {
643
- this.log.warn(`Failed to create block proposal, skipping collecting attestations`);
644
- return undefined;
633
+ const msg = `Failed to create block proposal`;
634
+ throw new Error(msg);
645
635
  }
646
636
 
647
637
  this.log.debug('Broadcasting block proposal to validators');
@@ -1,5 +1,6 @@
1
1
  import { type Tx, TxExecutionPhase, type TxValidationResult, type TxValidator } from '@aztec/circuit-types';
2
- import { type AztecAddress, type Fr, FunctionSelector, type GasFees } from '@aztec/circuits.js';
2
+ import { type AztecAddress, Fr, FunctionSelector, type GasFees } from '@aztec/circuits.js';
3
+ import { U128 } from '@aztec/foundation/abi';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
5
  import { computeFeePayerBalanceStorageSlot, getExecutionRequestsByPhase } from '@aztec/simulator/server';
5
6
 
@@ -34,6 +35,12 @@ export class GasTxValidator implements TxValidator<Tx> {
34
35
  return this.#validateTxFee(tx);
35
36
  }
36
37
 
38
+ /**
39
+ * Check whether the tx's max fees are valid for the current block, and skip if not.
40
+ * We skip instead of invalidating since the tx may become elligible later.
41
+ * Note that circuits check max fees even if fee payer is unset, so we
42
+ * keep this validation even if the tx does not pay fees.
43
+ */
37
44
  #shouldSkip(tx: Tx): boolean {
38
45
  const gasSettings = tx.data.constants.txContext.gasSettings;
39
46
 
@@ -78,12 +85,17 @@ export class GasTxValidator implements TxValidator<Tx> {
78
85
  fn.callContext.msgSender.equals(this.#feeJuiceAddress) &&
79
86
  fn.args.length > 2 &&
80
87
  // Public functions get routed through the dispatch function, whose first argument is the target function selector.
81
- fn.args[0].equals(FunctionSelector.fromSignature('_increase_public_balance((Field),Field)').toField()) &&
88
+ fn.args[0].equals(
89
+ FunctionSelector.fromSignature('_increase_public_balance((Field),(Field,Field))').toField(),
90
+ ) &&
82
91
  fn.args[1].equals(feePayer.toField()) &&
83
92
  !fn.callContext.isStaticCall,
84
93
  );
85
94
 
86
- const balance = claimFunctionCall ? initialBalance.add(claimFunctionCall.args[2]) : initialBalance;
95
+ // `amount` in the claim function call arguments occupies 2 fields as it is represented as U128.
96
+ const balance = claimFunctionCall
97
+ ? initialBalance.add(new Fr(U128.fromFields(claimFunctionCall.args.slice(2, 4)).toInteger()))
98
+ : initialBalance;
87
99
  if (balance.lt(feeLimit)) {
88
100
  this.#log.warn(`Rejecting transaction due to not enough fee payer balance`, {
89
101
  feePayer,