@aztec/sequencer-client 0.0.1-commit.c80b6263 → 0.0.1-commit.cf93bcc56

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 (51) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +15 -4
  4. package/dest/config.d.ts +3 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +17 -12
  7. package/dest/global_variable_builder/global_builder.d.ts +2 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/publisher/config.d.ts +31 -17
  10. package/dest/publisher/config.d.ts.map +1 -1
  11. package/dest/publisher/config.js +101 -42
  12. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  13. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  14. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  15. package/dest/publisher/sequencer-publisher.d.ts +16 -8
  16. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher.js +80 -39
  18. package/dest/sequencer/checkpoint_proposal_job.d.ts +29 -6
  19. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  20. package/dest/sequencer/checkpoint_proposal_job.js +97 -53
  21. package/dest/sequencer/metrics.d.ts +17 -5
  22. package/dest/sequencer/metrics.d.ts.map +1 -1
  23. package/dest/sequencer/metrics.js +86 -15
  24. package/dest/sequencer/sequencer.d.ts +12 -7
  25. package/dest/sequencer/sequencer.d.ts.map +1 -1
  26. package/dest/sequencer/sequencer.js +24 -26
  27. package/dest/sequencer/timetable.d.ts +1 -4
  28. package/dest/sequencer/timetable.d.ts.map +1 -1
  29. package/dest/sequencer/timetable.js +1 -4
  30. package/dest/test/index.d.ts +3 -5
  31. package/dest/test/index.d.ts.map +1 -1
  32. package/dest/test/mock_checkpoint_builder.d.ts +7 -5
  33. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  34. package/dest/test/mock_checkpoint_builder.js +6 -6
  35. package/dest/test/utils.d.ts +3 -3
  36. package/dest/test/utils.d.ts.map +1 -1
  37. package/dest/test/utils.js +5 -4
  38. package/package.json +28 -28
  39. package/src/client/sequencer-client.ts +25 -7
  40. package/src/config.ts +26 -19
  41. package/src/global_variable_builder/global_builder.ts +1 -1
  42. package/src/publisher/config.ts +112 -43
  43. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  44. package/src/publisher/sequencer-publisher.ts +96 -45
  45. package/src/sequencer/checkpoint_proposal_job.ts +134 -70
  46. package/src/sequencer/metrics.ts +92 -18
  47. package/src/sequencer/sequencer.ts +32 -31
  48. package/src/sequencer/timetable.ts +6 -5
  49. package/src/test/index.ts +2 -4
  50. package/src/test/mock_checkpoint_builder.ts +14 -5
  51. package/src/test/utils.ts +5 -2
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,11 +19,12 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
23
+ MAX_L1_TX_LIMIT,
21
24
  type TransactionStats,
22
25
  WEI_CONST,
23
26
  } from '@aztec/ethereum/l1-tx-utils';
24
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
@@ -31,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
31
33
  import { EthAddress } from '@aztec/foundation/eth-address';
32
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
34
37
  import { bufferToHex } from '@aztec/foundation/string';
35
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
36
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -44,7 +47,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
44
47
 
45
48
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
49
 
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import type { SequencerPublisherConfig } from './config.js';
48
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
52
 
50
53
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +62,8 @@ type L1ProcessArgs = {
59
62
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
63
  /** Attestations and signers signature */
61
64
  attestationsAndSignersSignature: Signature;
65
+ /** The fee asset price modifier in basis points (from oracle) */
66
+ feeAssetPriceModifier: bigint;
62
67
  };
63
68
 
64
69
  export const Actions = [
@@ -111,6 +116,7 @@ export class SequencerPublisher {
111
116
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
117
 
113
118
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
119
+ private payloadProposedCache: Set<string> = new Set<string>();
114
120
 
115
121
  protected log: Logger;
116
122
  protected ethereumSlotDuration: bigint;
@@ -122,10 +128,9 @@ export class SequencerPublisher {
122
128
 
123
129
  /** L1 fee analyzer for fisherman mode */
124
130
  private l1FeeAnalyzer?: L1FeeAnalyzer;
125
- // @note - with blobs, the below estimate seems too large.
126
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
127
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
128
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
131
+
132
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
133
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
134
 
130
135
  // A CALL to a cold address is 2700 gas
131
136
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,7 +138,7 @@ export class SequencerPublisher {
133
138
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
139
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
140
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
141
+ public l1TxUtils: L1TxUtils;
137
142
  public rollupContract: RollupContract;
138
143
  public govProposerContract: GovernanceProposerContract;
139
144
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -144,11 +149,12 @@ export class SequencerPublisher {
144
149
  protected requests: RequestWithExpiry[] = [];
145
150
 
146
151
  constructor(
147
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
152
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
153
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
148
154
  deps: {
149
155
  telemetry?: TelemetryClient;
150
156
  blobClient: BlobClientInterface;
151
- l1TxUtils: L1TxUtilsWithBlobs;
157
+ l1TxUtils: L1TxUtils;
152
158
  rollupContract: RollupContract;
153
159
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
154
160
  governanceProposerContract: GovernanceProposerContract;
@@ -192,12 +198,27 @@ export class SequencerPublisher {
192
198
  createLogger('sequencer:publisher:fee-analyzer'),
193
199
  );
194
200
  }
201
+
202
+ // Initialize fee asset price oracle
203
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
204
+ this.l1TxUtils.client,
205
+ this.rollupContract,
206
+ createLogger('sequencer:publisher:price-oracle'),
207
+ );
195
208
  }
196
209
 
197
210
  public getRollupContract(): RollupContract {
198
211
  return this.rollupContract;
199
212
  }
200
213
 
214
+ /**
215
+ * Gets the fee asset price modifier from the oracle.
216
+ * Returns 0n if the oracle query fails.
217
+ */
218
+ public getFeeAssetPriceModifier(): Promise<bigint> {
219
+ return this.feeAssetPriceOracle.computePriceModifier();
220
+ }
221
+
201
222
  public getSenderAddress() {
202
223
  return this.l1TxUtils.getSenderAddress();
203
224
  }
@@ -273,7 +294,7 @@ export class SequencerPublisher {
273
294
  // Start the analysis
274
295
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
296
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
297
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
298
  l1Requests,
278
299
  blobConfig,
279
300
  onComplete,
@@ -346,7 +367,16 @@ export class SequencerPublisher {
346
367
 
347
368
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
369
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
370
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
371
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
372
+ const maxGas = MAX_L1_TX_LIMIT;
373
+ if (gasLimit !== undefined && gasLimit > maxGas) {
374
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
375
+ requested: gasLimit,
376
+ capped: maxGas,
377
+ });
378
+ gasLimit = maxGas;
379
+ }
350
380
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
381
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
382
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -517,7 +547,12 @@ export class SequencerPublisher {
517
547
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
548
 
519
549
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
550
+ const { gasUsed } = await this.l1TxUtils.simulate(
551
+ request,
552
+ undefined,
553
+ undefined,
554
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
555
+ );
521
556
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
557
  ...logData,
523
558
  request,
@@ -536,7 +571,7 @@ export class SequencerPublisher {
536
571
 
537
572
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
573
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
574
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
575
  this.log.verbose(
541
576
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
577
  { ...logData, request, error: viemError.message },
@@ -607,24 +642,8 @@ export class SequencerPublisher {
607
642
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
608
643
  ): Promise<bigint> {
609
644
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
610
-
611
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
612
- // If we have no attestations, we still need to provide the empty attestations
613
- // so that the committee is recalculated correctly
614
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
615
- // if (ignoreSignatures) {
616
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
617
- // if (!committee) {
618
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
619
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
620
- // }
621
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
622
- // CommitteeAttestation.fromAddress(committeeMember),
623
- // );
624
- // }
625
-
626
645
  const blobFields = checkpoint.toBlobFields();
627
- const blobs = getBlobsPerL1Block(blobFields);
646
+ const blobs = await getBlobsPerL1Block(blobFields);
628
647
  const blobInput = getPrefixedEthBlobCommitments(blobs);
629
648
 
630
649
  const args = [
@@ -632,7 +651,7 @@ export class SequencerPublisher {
632
651
  header: checkpoint.header.toViem(),
633
652
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
653
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
654
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
655
  },
637
656
  },
638
657
  attestationsAndSigners.getPackedAttestations(),
@@ -681,6 +700,32 @@ export class SequencerPublisher {
681
700
  return false;
682
701
  }
683
702
 
703
+ // Check if payload was already submitted to governance
704
+ const cacheKey = payload.toString();
705
+ if (!this.payloadProposedCache.has(cacheKey)) {
706
+ try {
707
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
708
+ const proposed = await retry(
709
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
710
+ 'Check if payload was proposed',
711
+ makeBackoff([0, 1, 2]),
712
+ this.log,
713
+ true,
714
+ );
715
+ if (proposed) {
716
+ this.payloadProposedCache.add(cacheKey);
717
+ }
718
+ } catch (err) {
719
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
720
+ return false;
721
+ }
722
+ }
723
+
724
+ if (this.payloadProposedCache.has(cacheKey)) {
725
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
726
+ return false;
727
+ }
728
+
684
729
  const cachedLastVote = this.lastActions[signalType];
685
730
  this.lastActions[signalType] = slotNumber;
686
731
  const action = signalType;
@@ -700,7 +745,7 @@ export class SequencerPublisher {
700
745
  });
701
746
 
702
747
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
748
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
749
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
750
  } catch (err) {
706
751
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
@@ -908,14 +953,15 @@ export class SequencerPublisher {
908
953
  const checkpointHeader = checkpoint.header;
909
954
 
910
955
  const blobFields = checkpoint.toBlobFields();
911
- const blobs = getBlobsPerL1Block(blobFields);
956
+ const blobs = await getBlobsPerL1Block(blobFields);
912
957
 
913
- const proposeTxArgs = {
958
+ const proposeTxArgs: L1ProcessArgs = {
914
959
  header: checkpointHeader,
915
960
  archive: checkpoint.archive.root.toBuffer(),
916
961
  blobs,
917
962
  attestationsAndSigners,
918
963
  attestationsAndSignersSignature,
964
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
965
  };
920
966
 
921
967
  let ts: bigint;
@@ -999,12 +1045,14 @@ export class SequencerPublisher {
999
1045
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1046
 
1001
1047
  let gasUsed: bigint;
1048
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1002
1049
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1050
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1004
1051
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1052
  } catch (err) {
1006
- const viemError = formatViemError(err);
1053
+ const viemError = formatViemError(err, simulateAbi);
1007
1054
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
+
1008
1056
  return false;
1009
1057
  }
1010
1058
 
@@ -1012,10 +1060,14 @@ export class SequencerPublisher {
1012
1060
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1061
  logData.gasLimit = gasLimit;
1014
1062
 
1063
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1064
+ // when the tx is sent and a revert is diagnosed via simulation.
1065
+ const requestWithAbi = { ...request, abi: simulateAbi };
1066
+
1015
1067
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1068
  this.addRequest({
1017
1069
  action,
1018
- request,
1070
+ request: requestWithAbi,
1019
1071
  gasConfig: { gasLimit },
1020
1072
  lastValidL2Slot: slotNumber,
1021
1073
  checkSuccess: (_req, result) => {
@@ -1097,8 +1149,7 @@ export class SequencerPublisher {
1097
1149
  header: encodedData.header.toViem(),
1098
1150
  archive: toHex(encodedData.archive),
1099
1151
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1152
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1153
  },
1103
1154
  },
1104
1155
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1124,7 +1175,7 @@ export class SequencerPublisher {
1124
1175
  readonly header: ViemHeader;
1125
1176
  readonly archive: `0x${string}`;
1126
1177
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1178
+ readonly feeAssetPriceModifier: bigint;
1128
1179
  };
1129
1180
  },
1130
1181
  ViemCommitteeAttestations,
@@ -1171,20 +1222,20 @@ export class SequencerPublisher {
1171
1222
  {
1172
1223
  to: this.rollupContract.address,
1173
1224
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1225
+ gas: MAX_L1_TX_LIMIT,
1175
1226
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1227
  },
1177
1228
  {
1178
1229
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
1230
  time: timestamp + 1n,
1180
1231
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1181
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1232
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1233
  },
1183
1234
  stateOverrides,
1184
1235
  RollupAbi,
1185
1236
  {
1186
1237
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1238
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1239
  },
1189
1240
  )
1190
1241
  .catch(err => {
@@ -1194,7 +1245,7 @@ export class SequencerPublisher {
1194
1245
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1246
  // Return a minimal simulation result with the fallback gas estimate
1196
1247
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1248
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1249
  logs: [],
1199
1250
  };
1200
1251
  }