@aztec/sequencer-client 1.2.1 → 2.0.0-nightly.20250813

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,29 +1,31 @@
1
1
  import { Blob } from '@aztec/blob-lib';
2
2
  import { createBlobSinkClient } from '@aztec/blob-sink/client';
3
- import { FormattedViemError, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract, formatViemError } from '@aztec/ethereum';
3
+ import { FormattedViemError, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract, formatViemError, tryExtractEvent } from '@aztec/ethereum';
4
4
  import { sumBigint } from '@aztec/foundation/bigint';
5
5
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
6
6
  import { EthAddress } from '@aztec/foundation/eth-address';
7
7
  import { createLogger } from '@aztec/foundation/log';
8
8
  import { Timer } from '@aztec/foundation/timer';
9
- import { RollupAbi } from '@aztec/l1-artifacts';
9
+ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
10
10
  import { CommitteeAttestation } from '@aztec/stdlib/block';
11
11
  import { ConsensusPayload, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
12
12
  import { getTelemetryClient } from '@aztec/telemetry-client';
13
13
  import pick from 'lodash.pick';
14
- import { encodeFunctionData, multicall3Abi, toHex } from 'viem';
14
+ import { encodeFunctionData, toHex } from 'viem';
15
15
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
16
- export var VoteType = /*#__PURE__*/ function(VoteType) {
17
- VoteType[VoteType["GOVERNANCE"] = 0] = "GOVERNANCE";
18
- VoteType[VoteType["SLASHING"] = 1] = "SLASHING";
19
- return VoteType;
16
+ export var SignalType = /*#__PURE__*/ function(SignalType) {
17
+ SignalType[SignalType["GOVERNANCE"] = 0] = "GOVERNANCE";
18
+ SignalType[SignalType["SLASHING"] = 1] = "SLASHING";
19
+ return SignalType;
20
20
  }({});
21
21
  const Actions = [
22
22
  'propose',
23
- 'governance-vote',
24
- 'slashing-vote'
23
+ 'governance-signal',
24
+ 'slashing-signal',
25
+ 'invalidate-by-invalid-attestation',
26
+ 'invalidate-by-insufficient-attestations'
25
27
  ];
26
- // Sorting for actions such that proposals always go first
28
+ // Sorting for actions such that invalidations go first, then proposals, and last votes
27
29
  const compareActions = (a, b)=>Actions.indexOf(b) - Actions.indexOf(a);
28
30
  export class SequencerPublisher {
29
31
  config;
@@ -31,12 +33,10 @@ export class SequencerPublisher {
31
33
  metrics;
32
34
  epochCache;
33
35
  governanceLog;
34
- governanceProposerAddress;
35
36
  governancePayload;
36
37
  slashingLog;
37
- slashingProposerAddress;
38
38
  getSlashPayload;
39
- myLastVotes;
39
+ myLastSignals;
40
40
  log;
41
41
  ethereumSlotDuration;
42
42
  blobSinkClient;
@@ -60,7 +60,7 @@ export class SequencerPublisher {
60
60
  this.governancePayload = EthAddress.ZERO;
61
61
  this.slashingLog = createLogger('sequencer:publisher:slashing');
62
62
  this.getSlashPayload = undefined;
63
- this.myLastVotes = {
63
+ this.myLastSignals = {
64
64
  [0]: 0n,
65
65
  [1]: 0n
66
66
  };
@@ -77,6 +77,11 @@ export class SequencerPublisher {
77
77
  this.rollupContract = deps.rollupContract;
78
78
  this.govProposerContract = deps.governanceProposerContract;
79
79
  this.slashingProposerContract = deps.slashingProposerContract;
80
+ this.rollupContract.listenToSlasherChanged(async ()=>{
81
+ this.log.info('Slashing proposer changed');
82
+ const newSlashingProposer = await this.rollupContract.getSlashingProposer();
83
+ this.slashingProposerContract = newSlashingProposer;
84
+ });
80
85
  }
81
86
  getRollupContract() {
82
87
  return this.rollupContract;
@@ -160,11 +165,13 @@ export class SequencerPublisher {
160
165
  validRequests: validRequests.map((request)=>request.action)
161
166
  });
162
167
  const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, gasConfig, blobConfig, this.rollupContract.address, this.log);
163
- this.callbackBundledTransactions(validRequests, result);
168
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
164
169
  return {
165
170
  result,
166
171
  expiredActions,
167
- validActions
172
+ sentActions: validActions,
173
+ successfulActions,
174
+ failedActions
168
175
  };
169
176
  } catch (err) {
170
177
  const viemError = formatViemError(err);
@@ -179,31 +186,44 @@ export class SequencerPublisher {
179
186
  }
180
187
  }
181
188
  callbackBundledTransactions(requests, result) {
182
- const isError = result instanceof FormattedViemError;
183
- const success = isError ? false : result?.receipt.status === 'success';
184
- const logger = success ? this.log.info : this.log.error;
185
- for (const request of requests){
186
- logger(`Bundled [${request.action}] transaction [${success ? 'succeeded' : 'failed'}]`);
187
- if (!isError) {
188
- request.onResult?.(request.request, result);
189
+ const actionsListStr = requests.map((r)=>r.action).join(', ');
190
+ if (result instanceof FormattedViemError) {
191
+ this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
192
+ return {
193
+ failedActions: requests.map((r)=>r.action)
194
+ };
195
+ } else {
196
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
197
+ result,
198
+ requests
199
+ });
200
+ const successfulActions = [];
201
+ const failedActions = [];
202
+ for (const request of requests){
203
+ if (request.checkSuccess(request.request, result)) {
204
+ successfulActions.push(request.action);
205
+ } else {
206
+ failedActions.push(request.action);
207
+ }
189
208
  }
190
- }
191
- if (isError) {
192
- this.log.error('Failed to publish bundled transactions', result);
209
+ return {
210
+ successfulActions,
211
+ failedActions
212
+ };
193
213
  }
194
214
  }
195
215
  /**
196
216
  * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
197
217
  * @param tipArchive - The archive to check
198
218
  * @returns The slot and block number if it is possible to propose, undefined otherwise
199
- */ canProposeAtNextEthBlock(tipArchive, msgSender) {
219
+ */ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
200
220
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
201
221
  const ignoredErrors = [
202
222
  'SlotAlreadyInChain',
203
223
  'InvalidProposer',
204
224
  'InvalidArchive'
205
225
  ];
206
- return this.rollupContract.canProposeAtNextEthBlock(tipArchive, msgSender.toString(), this.ethereumSlotDuration).catch((err)=>{
226
+ return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, opts).catch((err)=>{
207
227
  if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
208
228
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
209
229
  error: err.message
@@ -219,7 +239,7 @@ export class SequencerPublisher {
219
239
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
220
240
  * It will throw if the block header is invalid.
221
241
  * @param header - The block header to validate
222
- */ async validateBlockHeader(header) {
242
+ */ async validateBlockHeader(header, opts) {
223
243
  const flags = {
224
244
  ignoreDA: true,
225
245
  ignoreSignatures: true
@@ -227,6 +247,7 @@ export class SequencerPublisher {
227
247
  const args = [
228
248
  header.toViem(),
229
249
  RollupContract.packAttestations([]),
250
+ [],
230
251
  `0x${'0'.repeat(64)}`,
231
252
  header.contentCommitment.blobsHash.toString(),
232
253
  flags
@@ -238,7 +259,7 @@ export class SequencerPublisher {
238
259
  to: this.rollupContract.address,
239
260
  data: encodeFunctionData({
240
261
  abi: RollupAbi,
241
- functionName: 'validateHeader',
262
+ functionName: 'validateHeaderWithAttestations',
242
263
  args
243
264
  }),
244
265
  from: MULTI_CALL_3_ADDRESS
@@ -248,10 +269,97 @@ export class SequencerPublisher {
248
269
  {
249
270
  address: MULTI_CALL_3_ADDRESS,
250
271
  balance
251
- }
272
+ },
273
+ ...await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)
252
274
  ]);
253
275
  }
254
276
  /**
277
+ * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
278
+ * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
279
+ */ async simulateInvalidateBlock(validationResult) {
280
+ if (validationResult.valid) {
281
+ return undefined;
282
+ }
283
+ const { reason, block } = validationResult;
284
+ const blockNumber = block.block.number;
285
+ const logData = {
286
+ ...block.block.toBlockInfo(),
287
+ reason
288
+ };
289
+ const currentBlockNumber = await this.rollupContract.getBlockNumber();
290
+ if (currentBlockNumber < validationResult.block.block.number) {
291
+ this.log.verbose(`Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`, {
292
+ currentBlockNumber,
293
+ ...logData
294
+ });
295
+ return undefined;
296
+ }
297
+ const request = this.buildInvalidateBlockRequest(validationResult);
298
+ this.log.debug(`Simulating invalidate block ${blockNumber}`, logData);
299
+ try {
300
+ const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
301
+ this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, {
302
+ ...logData,
303
+ request,
304
+ gasUsed
305
+ });
306
+ return {
307
+ request,
308
+ gasUsed,
309
+ blockNumber,
310
+ forcePendingBlockNumber: blockNumber - 1,
311
+ reason
312
+ };
313
+ } catch (err) {
314
+ const viemError = formatViemError(err);
315
+ // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
316
+ // we can safely ignore it and return undefined so we go ahead with block building.
317
+ if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
318
+ this.log.verbose(`Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`, {
319
+ ...logData,
320
+ request,
321
+ error: viemError.message
322
+ });
323
+ const latestPendingBlockNumber = await this.rollupContract.getBlockNumber();
324
+ if (latestPendingBlockNumber < blockNumber) {
325
+ this.log.verbose(`Block number ${blockNumber} has already been invalidated`, {
326
+ ...logData
327
+ });
328
+ return undefined;
329
+ } else {
330
+ this.log.error(`Simulation for invalidate ${blockNumber} failed and it is still in pending chain`, viemError, logData);
331
+ throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
332
+ cause: viemError
333
+ });
334
+ }
335
+ }
336
+ // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
337
+ this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
338
+ throw new Error(`Failed to simulate invalidate block ${blockNumber}`, {
339
+ cause: viemError
340
+ });
341
+ }
342
+ }
343
+ buildInvalidateBlockRequest(validationResult) {
344
+ if (validationResult.valid) {
345
+ throw new Error('Cannot invalidate a valid block');
346
+ }
347
+ const { block, committee, reason } = validationResult;
348
+ const logData = {
349
+ ...block.block.toBlockInfo(),
350
+ reason
351
+ };
352
+ this.log.debug(`Simulating invalidate block ${block.block.number}`, logData);
353
+ if (reason === 'invalid-attestation') {
354
+ return this.rollupContract.buildInvalidateBadAttestationRequest(block.block.number, block.attestations.map((a)=>a.toViem()), committee, validationResult.invalidIndex);
355
+ } else if (reason === 'insufficient-attestations') {
356
+ return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(block.block.number, block.attestations.map((a)=>a.toViem()), committee);
357
+ } else {
358
+ const _ = reason;
359
+ throw new Error(`Unknown reason for invalidation`);
360
+ }
361
+ }
362
+ /**
255
363
  * @notice Will simulate `propose` to make sure that the block is valid for submission
256
364
  *
257
365
  * @dev Throws if unable to propose
@@ -262,7 +370,7 @@ export class SequencerPublisher {
262
370
  */ async validateBlockForSubmission(block, attestationData = {
263
371
  digest: Buffer.alloc(32),
264
372
  attestations: []
265
- }) {
373
+ }, options) {
266
374
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
267
375
  // If we have no attestations, we still need to provide the empty attestations
268
376
  // so that the committee is recalculated correctly
@@ -275,11 +383,10 @@ export class SequencerPublisher {
275
383
  }
276
384
  attestationData.attestations = committee.map((committeeMember)=>CommitteeAttestation.fromAddress(committeeMember));
277
385
  }
278
- // const blobs = await Blob.getBlobs(block.body.toBlobFields());
279
- // const blobInput = Blob.getEthBlobEvaluationInputs(blobs);
280
386
  const blobs = await Blob.getBlobsPerBlock(block.body.toBlobFields());
281
387
  const blobInput = Blob.getPrefixedEthBlobCommitments(blobs);
282
388
  const formattedAttestations = attestationData.attestations.map((attest)=>attest.toViem());
389
+ const signers = attestationData.attestations.filter((attest)=>!attest.signature.isEmpty()).map((attest)=>attest.address.toString());
283
390
  const args = [
284
391
  {
285
392
  header: block.header.toPropose().toViem(),
@@ -291,37 +398,49 @@ export class SequencerPublisher {
291
398
  }
292
399
  },
293
400
  RollupContract.packAttestations(formattedAttestations),
401
+ signers,
294
402
  blobInput
295
403
  ];
296
- await this.simulateProposeTx(args, ts);
404
+ await this.simulateProposeTx(args, ts, options);
297
405
  return ts;
298
406
  }
299
- async getCurrentEpochCommittee() {
300
- const committee = await this.rollupContract.getCurrentEpochCommittee();
301
- return committee?.map(EthAddress.fromString);
302
- }
303
- async enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base, signerAddress, signer) {
304
- if (this.myLastVotes[voteType] >= slotNumber) {
407
+ async enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer) {
408
+ if (this.myLastSignals[signalType] >= slotNumber) {
305
409
  return false;
306
410
  }
307
411
  if (payload.equals(EthAddress.ZERO)) {
308
412
  return false;
309
413
  }
414
+ if (signerAddress.equals(EthAddress.ZERO)) {
415
+ this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
416
+ return false;
417
+ }
310
418
  const round = await base.computeRound(slotNumber);
311
419
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
312
- if (roundInfo.lastVote >= slotNumber) {
420
+ if (roundInfo.lastSignalSlot >= slotNumber) {
313
421
  return false;
314
422
  }
315
- const cachedLastVote = this.myLastVotes[voteType];
316
- this.myLastVotes[voteType] = slotNumber;
317
- const action = voteType === 0 ? 'governance-vote' : 'slashing-vote';
318
- const request = await base.createVoteRequestWithSignature(payload.toString(), this.config.l1ChainId, signerAddress.toString(), signer);
423
+ const cachedLastVote = this.myLastSignals[signalType];
424
+ this.myLastSignals[signalType] = slotNumber;
425
+ const action = signalType === 0 ? 'governance-signal' : 'slashing-signal';
426
+ const request = await base.createSignalRequestWithSignature(payload.toString(), round, this.config.l1ChainId, signerAddress.toString(), signer);
319
427
  this.log.debug(`Created ${action} request with signature`, {
320
428
  request,
321
429
  round,
322
430
  signer: this.l1TxUtils.client.account?.address,
323
431
  lastValidL2Slot: slotNumber
324
432
  });
433
+ try {
434
+ await this.l1TxUtils.simulate(request, {
435
+ time: timestamp
436
+ }, [], ErrorsAbi);
437
+ this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
438
+ request
439
+ });
440
+ } catch (err) {
441
+ this.log.warn(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
442
+ // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
443
+ }
325
444
  this.addRequest({
326
445
  gasConfig: {
327
446
  gasLimit: SequencerPublisher.VOTE_GAS_GUESS
@@ -329,23 +448,33 @@ export class SequencerPublisher {
329
448
  action,
330
449
  request,
331
450
  lastValidL2Slot: slotNumber,
332
- onResult: (_request, result)=>{
333
- if (!result || result.receipt.status !== 'success') {
334
- this.myLastVotes[voteType] = cachedLastVote;
451
+ checkSuccess: (_request, result)=>{
452
+ const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, base.address.toString(), EmpireBaseAbi, 'SignalCast');
453
+ const logData = {
454
+ ...result,
455
+ slotNumber,
456
+ round,
457
+ payload: payload.toString()
458
+ };
459
+ if (!success) {
460
+ this.log.error(`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`, logData);
461
+ this.myLastSignals[signalType] = cachedLastVote;
462
+ return false;
335
463
  } else {
336
- this.log.info(`Voting in [${action}] for ${payload} at slot ${slotNumber} in round ${round}`);
464
+ this.log.info(`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`, logData);
465
+ return true;
337
466
  }
338
467
  }
339
468
  });
340
469
  return true;
341
470
  }
342
- async getVoteConfig(slotNumber, voteType) {
343
- if (voteType === 0) {
471
+ async getSignalConfig(slotNumber, signalType) {
472
+ if (signalType === 0) {
344
473
  return {
345
474
  payload: this.governancePayload,
346
475
  base: this.govProposerContract
347
476
  };
348
- } else if (voteType === 1) {
477
+ } else if (signalType === 1) {
349
478
  if (!this.getSlashPayload) {
350
479
  return undefined;
351
480
  }
@@ -358,26 +487,24 @@ export class SequencerPublisher {
358
487
  payload: slashPayload,
359
488
  base: this.slashingProposerContract
360
489
  };
490
+ } else {
491
+ const _ = signalType;
492
+ throw new Error('Unreachable: Invalid signal type');
361
493
  }
362
- throw new Error('Unreachable: Invalid vote type');
363
494
  }
364
495
  /**
365
- * Enqueues a castVote transaction to cast a vote for a given slot number.
366
- * @param slotNumber - The slot number to cast a vote for.
367
- * @param timestamp - The timestamp of the slot to cast a vote for.
368
- * @param voteType - The type of vote to cast.
369
- * @returns True if the vote was successfully enqueued, false otherwise.
370
- */ async enqueueCastVote(slotNumber, timestamp, voteType, signerAddress, signer) {
371
- const voteConfig = await this.getVoteConfig(slotNumber, voteType);
372
- if (!voteConfig) {
496
+ * Enqueues a castSignal transaction to cast a signal for a given slot number.
497
+ * @param slotNumber - The slot number to cast a signal for.
498
+ * @param timestamp - The timestamp of the slot to cast a signal for.
499
+ * @param signalType - The type of signal to cast.
500
+ * @returns True if the signal was successfully enqueued, false otherwise.
501
+ */ async enqueueCastSignal(slotNumber, timestamp, signalType, signerAddress, signer) {
502
+ const signalConfig = await this.getSignalConfig(slotNumber, signalType);
503
+ if (!signalConfig) {
373
504
  return false;
374
505
  }
375
- if (signerAddress.equals(EthAddress.ZERO)) {
376
- this.log.warn(`Cannot enqueue vote cast signal ${voteType} for address zero at slot ${slotNumber}`);
377
- return false;
378
- }
379
- const { payload, base } = voteConfig;
380
- return this.enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base, signerAddress, signer);
506
+ const { payload, base } = signalConfig;
507
+ return this.enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer);
381
508
  }
382
509
  /**
383
510
  * Proposes a L2 block on L1.
@@ -404,21 +531,64 @@ export class SequencerPublisher {
404
531
  // This means that we can avoid the simulation issues in later checks.
405
532
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
406
533
  // make time consistency checks break.
407
- ts = await this.validateBlockForSubmission(block, {
534
+ const attestationData = {
408
535
  digest: digest.toBuffer(),
409
536
  attestations: attestations ?? []
410
- });
537
+ };
538
+ // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
539
+ ts = await this.validateBlockForSubmission(block, attestationData, opts);
411
540
  } catch (err) {
412
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, undefined, {
541
+ this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
413
542
  ...block.getStats(),
414
- slotNumber: block.header.globalVariables.slotNumber.toBigInt()
543
+ slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
544
+ forcePendingBlockNumber: opts.forcePendingBlockNumber
415
545
  });
416
546
  throw err;
417
547
  }
418
- this.log.debug(`Submitting propose transaction`);
548
+ this.log.verbose(`Enqueuing block propose transaction`, {
549
+ ...block.toBlockInfo(),
550
+ ...opts
551
+ });
419
552
  await this.addProposeTx(block, proposeTxArgs, opts, ts);
420
553
  return true;
421
554
  }
555
+ enqueueInvalidateBlock(request, opts = {}) {
556
+ if (!request) {
557
+ return;
558
+ }
559
+ // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
560
+ const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(request.gasUsed) * 64 / 63)));
561
+ const logData = {
562
+ ...pick(request, 'gasUsed', 'blockNumber'),
563
+ gasLimit,
564
+ opts
565
+ };
566
+ this.log.verbose(`Enqueuing invalidate block request`, logData);
567
+ this.addRequest({
568
+ action: `invalidate-by-${request.reason}`,
569
+ request: request.request,
570
+ gasConfig: {
571
+ gasLimit,
572
+ txTimeoutAt: opts.txTimeoutAt
573
+ },
574
+ lastValidL2Slot: this.getCurrentL2Slot() + 2n,
575
+ checkSuccess: (_req, result)=>{
576
+ const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'BlockInvalidated');
577
+ if (!success) {
578
+ this.log.warn(`Invalidate block ${request.blockNumber} failed`, {
579
+ ...result,
580
+ ...logData
581
+ });
582
+ } else {
583
+ this.log.info(`Invalidate block ${request.blockNumber} succeeded`, {
584
+ ...result,
585
+ ...logData
586
+ });
587
+ }
588
+ return !!success;
589
+ }
590
+ });
591
+ }
422
592
  /**
423
593
  * Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
424
594
  * Be warned, the call may return false even if the tx subsequently gets successfully mined.
@@ -432,7 +602,7 @@ export class SequencerPublisher {
432
602
  this.interrupted = false;
433
603
  this.l1TxUtils.restart();
434
604
  }
435
- async prepareProposeTx(encodedData, timestamp) {
605
+ async prepareProposeTx(encodedData, timestamp, options) {
436
606
  if (!this.l1TxUtils.client.account) {
437
607
  throw new Error('L1 TX utils needs to be initialized with an account wallet.');
438
608
  }
@@ -462,6 +632,7 @@ export class SequencerPublisher {
462
632
  });
463
633
  const attestations = encodedData.attestations ? encodedData.attestations.map((attest)=>attest.toViem()) : [];
464
634
  const txHashes = encodedData.txHashes ? encodedData.txHashes.map((txHash)=>txHash.toString()) : [];
635
+ const signers = encodedData.attestations?.filter((attest)=>!attest.signature.isEmpty()).map((attest)=>attest.address.toString());
465
636
  const args = [
466
637
  {
467
638
  header: encodedData.header.toViem(),
@@ -474,9 +645,10 @@ export class SequencerPublisher {
474
645
  txHashes
475
646
  },
476
647
  RollupContract.packAttestations(attestations),
648
+ signers ?? [],
477
649
  blobInput
478
650
  ];
479
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp);
651
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
480
652
  return {
481
653
  args,
482
654
  blobEvaluationGas,
@@ -489,28 +661,17 @@ export class SequencerPublisher {
489
661
  * @param args - The propose tx args
490
662
  * @param timestamp - The timestamp to simulate proposal at
491
663
  * @returns The simulation result
492
- */ async simulateProposeTx(args, timestamp) {
664
+ */ async simulateProposeTx(args, timestamp, options) {
493
665
  const rollupData = encodeFunctionData({
494
666
  abi: RollupAbi,
495
667
  functionName: 'propose',
496
668
  args
497
669
  });
498
- const forwarderData = encodeFunctionData({
499
- abi: multicall3Abi,
500
- functionName: 'aggregate3',
501
- args: [
502
- [
503
- {
504
- target: this.rollupContract.address,
505
- allowFailure: false,
506
- callData: rollupData
507
- }
508
- ]
509
- ]
510
- });
670
+ // override the pending block number if requested
671
+ const forcePendingBlockNumberStateDiff = (options.forcePendingBlockNumber !== undefined ? await this.rollupContract.makePendingBlockNumberOverride(options.forcePendingBlockNumber) : []).flatMap((override)=>override.stateDiff ?? []);
511
672
  const simulationResult = await this.l1TxUtils.simulate({
512
- to: MULTI_CALL_3_ADDRESS,
513
- data: forwarderData,
673
+ to: this.rollupContract.address,
674
+ data: rollupData,
514
675
  gas: SequencerPublisher.PROPOSE_GAS_GUESS
515
676
  }, {
516
677
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
@@ -525,7 +686,8 @@ export class SequencerPublisher {
525
686
  {
526
687
  slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
527
688
  value: toPaddedHex(0n, true)
528
- }
689
+ },
690
+ ...forcePendingBlockNumberStateDiff
529
691
  ]
530
692
  }
531
693
  ], RollupAbi, {
@@ -543,8 +705,14 @@ export class SequencerPublisher {
543
705
  async addProposeTx(block, encodedData, opts = {}, timestamp) {
544
706
  const timer = new Timer();
545
707
  const kzg = Blob.getViemKzgInstance();
546
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp);
708
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
547
709
  const startBlock = await this.l1TxUtils.getBlockNumber();
710
+ const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
711
+ // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
712
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
713
+ void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch((_err)=>{
714
+ this.log.error('Failed to send blobs to blob sink');
715
+ });
548
716
  return this.addRequest({
549
717
  action: 'propose',
550
718
  request: {
@@ -554,18 +722,19 @@ export class SequencerPublisher {
554
722
  lastValidL2Slot: block.header.globalVariables.slotNumber.toBigInt(),
555
723
  gasConfig: {
556
724
  ...opts,
557
- gasLimit: this.l1TxUtils.bumpGasLimit(simulationResult.gasUsed + blobEvaluationGas)
725
+ gasLimit
558
726
  },
559
727
  blobConfig: {
560
728
  blobs: encodedData.blobs.map((b)=>b.data),
561
729
  kzg
562
730
  },
563
- onResult: (request, result)=>{
731
+ checkSuccess: (request, result)=>{
564
732
  if (!result) {
565
- return;
733
+ return false;
566
734
  }
567
735
  const { receipt, stats, errorMsg } = result;
568
- if (receipt.status === 'success') {
736
+ const success = receipt && receipt.status === 'success' && tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'L2BlockProposed');
737
+ if (success) {
569
738
  const endBlock = receipt.blockNumber;
570
739
  const inclusionBlocks = Number(endBlock - startBlock);
571
740
  const publishStats = {
@@ -580,35 +749,24 @@ export class SequencerPublisher {
580
749
  blobCount: encodedData.blobs.length,
581
750
  inclusionBlocks
582
751
  };
583
- this.log.verbose(`Published L2 block to L1 rollup contract`, {
752
+ this.log.info(`Published L2 block to L1 rollup contract`, {
584
753
  ...stats,
585
- ...block.getStats()
754
+ ...block.getStats(),
755
+ ...receipt
586
756
  });
587
757
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
588
- // Send the blobs to the blob sink
589
- this.sendBlobsToBlobSink(receipt.blockHash, encodedData.blobs).catch((_err)=>{
590
- this.log.error('Failed to send blobs to blob sink');
591
- });
592
758
  return true;
593
759
  } else {
594
760
  this.metrics.recordFailedTx('process');
595
- this.log.error(`Rollup process tx reverted. ${errorMsg ?? 'No error message'}`, undefined, {
761
+ this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
596
762
  ...block.getStats(),
763
+ receipt,
597
764
  txHash: receipt.transactionHash,
598
765
  slotNumber: block.header.globalVariables.slotNumber.toBigInt()
599
766
  });
767
+ return false;
600
768
  }
601
769
  }
602
770
  });
603
771
  }
604
- /**
605
- * Send blobs to the blob sink
606
- *
607
- * If a blob sink url is configured, then we send blobs to the blob sink
608
- * - for now we use the blockHash as the identifier for the blobs;
609
- * In the future this will move to be the beacon block id - which takes a bit more work
610
- * to calculate and will need to be mocked in e2e tests
611
- */ sendBlobsToBlobSink(blockHash, blobs) {
612
- return this.blobSinkClient.sendBlobsToBlobSink(blockHash, blobs);
613
- }
614
772
  }