@aztec/sequencer-client 1.2.1 → 2.0.0-nightly.20250814
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.
- package/dest/client/sequencer-client.d.ts +0 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +5 -7
- package/dest/config.d.ts +1 -0
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +27 -0
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +48 -33
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +272 -114
- package/dest/sequencer/block_builder.d.ts +2 -7
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +3 -7
- package/dest/sequencer/sequencer.d.ts +27 -15
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +141 -85
- package/dest/sequencer/timetable.d.ts +15 -5
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +25 -12
- package/dest/sequencer/utils.d.ts +1 -11
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +0 -23
- package/package.json +26 -26
- package/src/client/sequencer-client.ts +4 -8
- package/src/config.ts +33 -0
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +318 -131
- package/src/sequencer/block_builder.ts +15 -8
- package/src/sequencer/sequencer.ts +217 -87
- package/src/sequencer/timetable.ts +43 -7
- package/src/sequencer/utils.ts +6 -37
|
@@ -20,22 +20,24 @@ import {
|
|
|
20
20
|
type ViemHeader,
|
|
21
21
|
type ViemStateReference,
|
|
22
22
|
formatViemError,
|
|
23
|
+
tryExtractEvent,
|
|
23
24
|
} from '@aztec/ethereum';
|
|
24
25
|
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
25
26
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
26
27
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
27
28
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
29
|
+
import type { Fr } from '@aztec/foundation/fields';
|
|
28
30
|
import { createLogger } from '@aztec/foundation/log';
|
|
29
|
-
import { Timer } from '@aztec/foundation/timer';
|
|
30
|
-
import { RollupAbi } from '@aztec/l1-artifacts';
|
|
31
|
-
import { CommitteeAttestation } from '@aztec/stdlib/block';
|
|
31
|
+
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
32
|
+
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
33
|
+
import { CommitteeAttestation, type ValidateBlockResult } from '@aztec/stdlib/block';
|
|
32
34
|
import { ConsensusPayload, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
|
|
33
35
|
import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
|
|
34
36
|
import { type ProposedBlockHeader, StateReference, TxHash } from '@aztec/stdlib/tx';
|
|
35
37
|
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
36
38
|
|
|
37
39
|
import pick from 'lodash.pick';
|
|
38
|
-
import { type TransactionReceipt,
|
|
40
|
+
import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
|
|
39
41
|
|
|
40
42
|
import type { PublisherConfig, TxSenderConfig } from './config.js';
|
|
41
43
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
@@ -56,29 +58,43 @@ type L1ProcessArgs = {
|
|
|
56
58
|
attestations?: CommitteeAttestation[];
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
export enum
|
|
61
|
+
export enum SignalType {
|
|
60
62
|
GOVERNANCE,
|
|
61
63
|
SLASHING,
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
type GetSlashPayloadCallBack = (slotNumber: bigint) => Promise<EthAddress | undefined>;
|
|
65
67
|
|
|
66
|
-
const Actions = [
|
|
68
|
+
const Actions = [
|
|
69
|
+
'propose',
|
|
70
|
+
'governance-signal',
|
|
71
|
+
'slashing-signal',
|
|
72
|
+
'invalidate-by-invalid-attestation',
|
|
73
|
+
'invalidate-by-insufficient-attestations',
|
|
74
|
+
] as const;
|
|
67
75
|
export type Action = (typeof Actions)[number];
|
|
68
76
|
|
|
69
|
-
// Sorting for actions such that proposals
|
|
77
|
+
// Sorting for actions such that invalidations go first, then proposals, and last votes
|
|
70
78
|
const compareActions = (a: Action, b: Action) => Actions.indexOf(b) - Actions.indexOf(a);
|
|
71
79
|
|
|
80
|
+
export type InvalidateBlockRequest = {
|
|
81
|
+
request: L1TxRequest;
|
|
82
|
+
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
83
|
+
gasUsed: bigint;
|
|
84
|
+
blockNumber: number;
|
|
85
|
+
forcePendingBlockNumber: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
72
88
|
interface RequestWithExpiry {
|
|
73
89
|
action: Action;
|
|
74
90
|
request: L1TxRequest;
|
|
75
91
|
lastValidL2Slot: bigint;
|
|
76
92
|
gasConfig?: Pick<L1GasConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
77
93
|
blobConfig?: L1BlobInputs;
|
|
78
|
-
|
|
94
|
+
checkSuccess: (
|
|
79
95
|
request: L1TxRequest,
|
|
80
96
|
result?: { receipt: TransactionReceipt; gasPrice: GasPrice; stats?: TransactionStats; errorMsg?: string },
|
|
81
|
-
) =>
|
|
97
|
+
) => boolean;
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
export class SequencerPublisher {
|
|
@@ -87,16 +103,14 @@ export class SequencerPublisher {
|
|
|
87
103
|
public epochCache: EpochCache;
|
|
88
104
|
|
|
89
105
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
90
|
-
protected governanceProposerAddress?: EthAddress;
|
|
91
106
|
private governancePayload: EthAddress = EthAddress.ZERO;
|
|
92
107
|
|
|
93
108
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
94
|
-
protected slashingProposerAddress?: EthAddress;
|
|
95
109
|
private getSlashPayload?: GetSlashPayloadCallBack = undefined;
|
|
96
110
|
|
|
97
|
-
private
|
|
98
|
-
[
|
|
99
|
-
[
|
|
111
|
+
private myLastSignals: Record<SignalType, bigint> = {
|
|
112
|
+
[SignalType.GOVERNANCE]: 0n,
|
|
113
|
+
[SignalType.SLASHING]: 0n,
|
|
100
114
|
};
|
|
101
115
|
|
|
102
116
|
protected log = createLogger('sequencer:publisher');
|
|
@@ -131,6 +145,7 @@ export class SequencerPublisher {
|
|
|
131
145
|
slashingProposerContract: SlashingProposerContract;
|
|
132
146
|
governanceProposerContract: GovernanceProposerContract;
|
|
133
147
|
epochCache: EpochCache;
|
|
148
|
+
dateProvider: DateProvider;
|
|
134
149
|
},
|
|
135
150
|
) {
|
|
136
151
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
@@ -147,6 +162,12 @@ export class SequencerPublisher {
|
|
|
147
162
|
|
|
148
163
|
this.govProposerContract = deps.governanceProposerContract;
|
|
149
164
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
165
|
+
|
|
166
|
+
this.rollupContract.listenToSlasherChanged(async () => {
|
|
167
|
+
this.log.info('Slashing proposer changed');
|
|
168
|
+
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
169
|
+
this.slashingProposerContract = newSlashingProposer;
|
|
170
|
+
});
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
public getRollupContract(): RollupContract {
|
|
@@ -240,9 +261,7 @@ export class SequencerPublisher {
|
|
|
240
261
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
241
262
|
|
|
242
263
|
try {
|
|
243
|
-
this.log.debug('Forwarding transactions', {
|
|
244
|
-
validRequests: validRequests.map(request => request.action),
|
|
245
|
-
});
|
|
264
|
+
this.log.debug('Forwarding transactions', { validRequests: validRequests.map(request => request.action) });
|
|
246
265
|
const result = await Multicall3.forward(
|
|
247
266
|
validRequests.map(request => request.request),
|
|
248
267
|
this.l1TxUtils,
|
|
@@ -251,8 +270,8 @@ export class SequencerPublisher {
|
|
|
251
270
|
this.rollupContract.address,
|
|
252
271
|
this.log,
|
|
253
272
|
);
|
|
254
|
-
this.callbackBundledTransactions(validRequests, result);
|
|
255
|
-
return { result, expiredActions, validActions };
|
|
273
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
274
|
+
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
256
275
|
} catch (err) {
|
|
257
276
|
const viemError = formatViemError(err);
|
|
258
277
|
this.log.error(`Failed to publish bundled transactions`, viemError);
|
|
@@ -270,17 +289,22 @@ export class SequencerPublisher {
|
|
|
270
289
|
requests: RequestWithExpiry[],
|
|
271
290
|
result?: { receipt: TransactionReceipt; gasPrice: GasPrice } | FormattedViemError,
|
|
272
291
|
) {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
293
|
+
if (result instanceof FormattedViemError) {
|
|
294
|
+
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
295
|
+
return { failedActions: requests.map(r => r.action) };
|
|
296
|
+
} else {
|
|
297
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
298
|
+
const successfulActions: Action[] = [];
|
|
299
|
+
const failedActions: Action[] = [];
|
|
300
|
+
for (const request of requests) {
|
|
301
|
+
if (request.checkSuccess(request.request, result)) {
|
|
302
|
+
successfulActions.push(request.action);
|
|
303
|
+
} else {
|
|
304
|
+
failedActions.push(request.action);
|
|
305
|
+
}
|
|
280
306
|
}
|
|
281
|
-
|
|
282
|
-
if (isError) {
|
|
283
|
-
this.log.error('Failed to publish bundled transactions', result);
|
|
307
|
+
return { successfulActions, failedActions };
|
|
284
308
|
}
|
|
285
309
|
}
|
|
286
310
|
|
|
@@ -289,12 +313,16 @@ export class SequencerPublisher {
|
|
|
289
313
|
* @param tipArchive - The archive to check
|
|
290
314
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
291
315
|
*/
|
|
292
|
-
public canProposeAtNextEthBlock(
|
|
316
|
+
public canProposeAtNextEthBlock(
|
|
317
|
+
tipArchive: Fr,
|
|
318
|
+
msgSender: EthAddress,
|
|
319
|
+
opts: { forcePendingBlockNumber?: number } = {},
|
|
320
|
+
) {
|
|
293
321
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
294
322
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
295
323
|
|
|
296
324
|
return this.rollupContract
|
|
297
|
-
.canProposeAtNextEthBlock(tipArchive, msgSender.toString(), this.ethereumSlotDuration)
|
|
325
|
+
.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, opts)
|
|
298
326
|
.catch(err => {
|
|
299
327
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
300
328
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -312,12 +340,16 @@ export class SequencerPublisher {
|
|
|
312
340
|
* It will throw if the block header is invalid.
|
|
313
341
|
* @param header - The block header to validate
|
|
314
342
|
*/
|
|
315
|
-
public async validateBlockHeader(
|
|
343
|
+
public async validateBlockHeader(
|
|
344
|
+
header: ProposedBlockHeader,
|
|
345
|
+
opts?: { forcePendingBlockNumber: number | undefined },
|
|
346
|
+
) {
|
|
316
347
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
317
348
|
|
|
318
349
|
const args = [
|
|
319
350
|
header.toViem(),
|
|
320
351
|
RollupContract.packAttestations([]),
|
|
352
|
+
[], // no signers
|
|
321
353
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
322
354
|
header.contentCommitment.blobsHash.toString(),
|
|
323
355
|
flags,
|
|
@@ -330,21 +362,109 @@ export class SequencerPublisher {
|
|
|
330
362
|
await this.l1TxUtils.simulate(
|
|
331
363
|
{
|
|
332
364
|
to: this.rollupContract.address,
|
|
333
|
-
data: encodeFunctionData({ abi: RollupAbi, functionName: '
|
|
365
|
+
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
334
366
|
from: MULTI_CALL_3_ADDRESS,
|
|
335
367
|
},
|
|
336
|
-
{
|
|
337
|
-
time: ts + 1n,
|
|
338
|
-
},
|
|
368
|
+
{ time: ts + 1n },
|
|
339
369
|
[
|
|
340
|
-
{
|
|
341
|
-
|
|
342
|
-
balance,
|
|
343
|
-
},
|
|
370
|
+
{ address: MULTI_CALL_3_ADDRESS, balance },
|
|
371
|
+
...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
|
|
344
372
|
],
|
|
345
373
|
);
|
|
346
374
|
}
|
|
347
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
|
|
378
|
+
* @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
|
|
379
|
+
*/
|
|
380
|
+
public async simulateInvalidateBlock(
|
|
381
|
+
validationResult: ValidateBlockResult,
|
|
382
|
+
): Promise<InvalidateBlockRequest | undefined> {
|
|
383
|
+
if (validationResult.valid) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const { reason, block } = validationResult;
|
|
388
|
+
const blockNumber = block.block.number;
|
|
389
|
+
const logData = { ...block.block.toBlockInfo(), reason };
|
|
390
|
+
|
|
391
|
+
const currentBlockNumber = await this.rollupContract.getBlockNumber();
|
|
392
|
+
if (currentBlockNumber < validationResult.block.block.number) {
|
|
393
|
+
this.log.verbose(
|
|
394
|
+
`Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
|
|
395
|
+
{ currentBlockNumber, ...logData },
|
|
396
|
+
);
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const request = this.buildInvalidateBlockRequest(validationResult);
|
|
401
|
+
this.log.debug(`Simulating invalidate block ${blockNumber}`, logData);
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
|
|
405
|
+
this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
|
|
406
|
+
|
|
407
|
+
return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const viemError = formatViemError(err);
|
|
410
|
+
|
|
411
|
+
// If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
|
|
412
|
+
// we can safely ignore it and return undefined so we go ahead with block building.
|
|
413
|
+
if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
|
|
414
|
+
this.log.verbose(
|
|
415
|
+
`Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
|
|
416
|
+
{ ...logData, request, error: viemError.message },
|
|
417
|
+
);
|
|
418
|
+
const latestPendingBlockNumber = await this.rollupContract.getBlockNumber();
|
|
419
|
+
if (latestPendingBlockNumber < blockNumber) {
|
|
420
|
+
this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
|
|
421
|
+
return undefined;
|
|
422
|
+
} else {
|
|
423
|
+
this.log.error(
|
|
424
|
+
`Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
|
|
425
|
+
viemError,
|
|
426
|
+
logData,
|
|
427
|
+
);
|
|
428
|
+
throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
|
|
429
|
+
cause: viemError,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
|
|
435
|
+
this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
|
|
436
|
+
throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
|
|
441
|
+
if (validationResult.valid) {
|
|
442
|
+
throw new Error('Cannot invalidate a valid block');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const { block, committee, reason } = validationResult;
|
|
446
|
+
const logData = { ...block.block.toBlockInfo(), reason };
|
|
447
|
+
this.log.debug(`Simulating invalidate block ${block.block.number}`, logData);
|
|
448
|
+
|
|
449
|
+
if (reason === 'invalid-attestation') {
|
|
450
|
+
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
451
|
+
block.block.number,
|
|
452
|
+
block.attestations.map(a => a.toViem()),
|
|
453
|
+
committee,
|
|
454
|
+
validationResult.invalidIndex,
|
|
455
|
+
);
|
|
456
|
+
} else if (reason === 'insufficient-attestations') {
|
|
457
|
+
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
458
|
+
block.block.number,
|
|
459
|
+
block.attestations.map(a => a.toViem()),
|
|
460
|
+
committee,
|
|
461
|
+
);
|
|
462
|
+
} else {
|
|
463
|
+
const _: never = reason;
|
|
464
|
+
throw new Error(`Unknown reason for invalidation`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
348
468
|
/**
|
|
349
469
|
* @notice Will simulate `propose` to make sure that the block is valid for submission
|
|
350
470
|
*
|
|
@@ -360,6 +480,7 @@ export class SequencerPublisher {
|
|
|
360
480
|
digest: Buffer.alloc(32),
|
|
361
481
|
attestations: [],
|
|
362
482
|
},
|
|
483
|
+
options: { forcePendingBlockNumber?: number },
|
|
363
484
|
): Promise<bigint> {
|
|
364
485
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
365
486
|
|
|
@@ -376,13 +497,14 @@ export class SequencerPublisher {
|
|
|
376
497
|
CommitteeAttestation.fromAddress(committeeMember),
|
|
377
498
|
);
|
|
378
499
|
}
|
|
379
|
-
// const blobs = await Blob.getBlobs(block.body.toBlobFields());
|
|
380
|
-
// const blobInput = Blob.getEthBlobEvaluationInputs(blobs);
|
|
381
500
|
|
|
382
501
|
const blobs = await Blob.getBlobsPerBlock(block.body.toBlobFields());
|
|
383
502
|
const blobInput = Blob.getPrefixedEthBlobCommitments(blobs);
|
|
384
503
|
|
|
385
504
|
const formattedAttestations = attestationData.attestations.map(attest => attest.toViem());
|
|
505
|
+
const signers = attestationData.attestations
|
|
506
|
+
.filter(attest => !attest.signature.isEmpty())
|
|
507
|
+
.map(attest => attest.address.toString());
|
|
386
508
|
|
|
387
509
|
const args = [
|
|
388
510
|
{
|
|
@@ -395,47 +517,48 @@ export class SequencerPublisher {
|
|
|
395
517
|
},
|
|
396
518
|
},
|
|
397
519
|
RollupContract.packAttestations(formattedAttestations),
|
|
520
|
+
signers,
|
|
398
521
|
blobInput,
|
|
399
522
|
] as const;
|
|
400
523
|
|
|
401
|
-
await this.simulateProposeTx(args, ts);
|
|
524
|
+
await this.simulateProposeTx(args, ts, options);
|
|
402
525
|
return ts;
|
|
403
526
|
}
|
|
404
527
|
|
|
405
|
-
|
|
406
|
-
const committee = await this.rollupContract.getCurrentEpochCommittee();
|
|
407
|
-
return committee?.map(EthAddress.fromString);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
private async enqueueCastVoteHelper(
|
|
528
|
+
private async enqueueCastSignalHelper(
|
|
411
529
|
slotNumber: bigint,
|
|
412
530
|
timestamp: bigint,
|
|
413
|
-
|
|
531
|
+
signalType: SignalType,
|
|
414
532
|
payload: EthAddress,
|
|
415
533
|
base: IEmpireBase,
|
|
416
534
|
signerAddress: EthAddress,
|
|
417
|
-
signer: (msg:
|
|
535
|
+
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
418
536
|
): Promise<boolean> {
|
|
419
|
-
if (this.
|
|
537
|
+
if (this.myLastSignals[signalType] >= slotNumber) {
|
|
420
538
|
return false;
|
|
421
539
|
}
|
|
422
540
|
if (payload.equals(EthAddress.ZERO)) {
|
|
423
541
|
return false;
|
|
424
542
|
}
|
|
543
|
+
if (signerAddress.equals(EthAddress.ZERO)) {
|
|
544
|
+
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
425
547
|
const round = await base.computeRound(slotNumber);
|
|
426
548
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
427
549
|
|
|
428
|
-
if (roundInfo.
|
|
550
|
+
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
429
551
|
return false;
|
|
430
552
|
}
|
|
431
553
|
|
|
432
|
-
const cachedLastVote = this.
|
|
433
|
-
this.
|
|
554
|
+
const cachedLastVote = this.myLastSignals[signalType];
|
|
555
|
+
this.myLastSignals[signalType] = slotNumber;
|
|
434
556
|
|
|
435
|
-
const action =
|
|
557
|
+
const action = signalType === SignalType.GOVERNANCE ? 'governance-signal' : 'slashing-signal';
|
|
436
558
|
|
|
437
|
-
const request = await base.
|
|
559
|
+
const request = await base.createSignalRequestWithSignature(
|
|
438
560
|
payload.toString(),
|
|
561
|
+
round,
|
|
439
562
|
this.config.l1ChainId,
|
|
440
563
|
signerAddress.toString(),
|
|
441
564
|
signer,
|
|
@@ -447,29 +570,53 @@ export class SequencerPublisher {
|
|
|
447
570
|
lastValidL2Slot: slotNumber,
|
|
448
571
|
});
|
|
449
572
|
|
|
573
|
+
try {
|
|
574
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
575
|
+
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
576
|
+
} catch (err) {
|
|
577
|
+
this.log.warn(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
|
|
578
|
+
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
579
|
+
}
|
|
580
|
+
|
|
450
581
|
this.addRequest({
|
|
451
582
|
gasConfig: { gasLimit: SequencerPublisher.VOTE_GAS_GUESS },
|
|
452
583
|
action,
|
|
453
584
|
request,
|
|
454
585
|
lastValidL2Slot: slotNumber,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
586
|
+
checkSuccess: (_request, result) => {
|
|
587
|
+
const success =
|
|
588
|
+
result &&
|
|
589
|
+
result.receipt &&
|
|
590
|
+
result.receipt.status === 'success' &&
|
|
591
|
+
tryExtractEvent(result.receipt.logs, base.address.toString(), EmpireBaseAbi, 'SignalCast');
|
|
592
|
+
|
|
593
|
+
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
594
|
+
if (!success) {
|
|
595
|
+
this.log.error(
|
|
596
|
+
`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
597
|
+
logData,
|
|
598
|
+
);
|
|
599
|
+
this.myLastSignals[signalType] = cachedLastVote;
|
|
600
|
+
return false;
|
|
458
601
|
} else {
|
|
459
|
-
this.log.info(
|
|
602
|
+
this.log.info(
|
|
603
|
+
`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
604
|
+
logData,
|
|
605
|
+
);
|
|
606
|
+
return true;
|
|
460
607
|
}
|
|
461
608
|
},
|
|
462
609
|
});
|
|
463
610
|
return true;
|
|
464
611
|
}
|
|
465
612
|
|
|
466
|
-
private async
|
|
613
|
+
private async getSignalConfig(
|
|
467
614
|
slotNumber: bigint,
|
|
468
|
-
|
|
615
|
+
signalType: SignalType,
|
|
469
616
|
): Promise<{ payload: EthAddress; base: IEmpireBase } | undefined> {
|
|
470
|
-
if (
|
|
617
|
+
if (signalType === SignalType.GOVERNANCE) {
|
|
471
618
|
return { payload: this.governancePayload, base: this.govProposerContract };
|
|
472
|
-
} else if (
|
|
619
|
+
} else if (signalType === SignalType.SLASHING) {
|
|
473
620
|
if (!this.getSlashPayload) {
|
|
474
621
|
return undefined;
|
|
475
622
|
}
|
|
@@ -479,34 +626,32 @@ export class SequencerPublisher {
|
|
|
479
626
|
}
|
|
480
627
|
this.log.info(`Slash payload: ${slashPayload}`);
|
|
481
628
|
return { payload: slashPayload, base: this.slashingProposerContract };
|
|
629
|
+
} else {
|
|
630
|
+
const _: never = signalType;
|
|
631
|
+
throw new Error('Unreachable: Invalid signal type');
|
|
482
632
|
}
|
|
483
|
-
throw new Error('Unreachable: Invalid vote type');
|
|
484
633
|
}
|
|
485
634
|
|
|
486
635
|
/**
|
|
487
|
-
* Enqueues a
|
|
488
|
-
* @param slotNumber - The slot number to cast a
|
|
489
|
-
* @param timestamp - The timestamp of the slot to cast a
|
|
490
|
-
* @param
|
|
491
|
-
* @returns True if the
|
|
636
|
+
* Enqueues a castSignal transaction to cast a signal for a given slot number.
|
|
637
|
+
* @param slotNumber - The slot number to cast a signal for.
|
|
638
|
+
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
639
|
+
* @param signalType - The type of signal to cast.
|
|
640
|
+
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
492
641
|
*/
|
|
493
|
-
public async
|
|
642
|
+
public async enqueueCastSignal(
|
|
494
643
|
slotNumber: bigint,
|
|
495
644
|
timestamp: bigint,
|
|
496
|
-
|
|
645
|
+
signalType: SignalType,
|
|
497
646
|
signerAddress: EthAddress,
|
|
498
|
-
signer: (msg:
|
|
647
|
+
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
499
648
|
): Promise<boolean> {
|
|
500
|
-
const
|
|
501
|
-
if (!
|
|
649
|
+
const signalConfig = await this.getSignalConfig(slotNumber, signalType);
|
|
650
|
+
if (!signalConfig) {
|
|
502
651
|
return false;
|
|
503
652
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
return false;
|
|
507
|
-
}
|
|
508
|
-
const { payload, base } = voteConfig;
|
|
509
|
-
return this.enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base, signerAddress, signer);
|
|
653
|
+
const { payload, base } = signalConfig;
|
|
654
|
+
return this.enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer);
|
|
510
655
|
}
|
|
511
656
|
|
|
512
657
|
/**
|
|
@@ -519,7 +664,7 @@ export class SequencerPublisher {
|
|
|
519
664
|
block: L2Block,
|
|
520
665
|
attestations?: CommitteeAttestation[],
|
|
521
666
|
txHashes?: TxHash[],
|
|
522
|
-
opts: { txTimeoutAt?: Date } = {},
|
|
667
|
+
opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
|
|
523
668
|
): Promise<boolean> {
|
|
524
669
|
const proposedBlockHeader = block.header.toPropose();
|
|
525
670
|
|
|
@@ -544,23 +689,54 @@ export class SequencerPublisher {
|
|
|
544
689
|
// This means that we can avoid the simulation issues in later checks.
|
|
545
690
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
546
691
|
// make time consistency checks break.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
});
|
|
692
|
+
const attestationData = { digest: digest.toBuffer(), attestations: attestations ?? [] };
|
|
693
|
+
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
694
|
+
ts = await this.validateBlockForSubmission(block, attestationData, opts);
|
|
551
695
|
} catch (err: any) {
|
|
552
|
-
this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`,
|
|
696
|
+
this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
553
697
|
...block.getStats(),
|
|
554
698
|
slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
|
|
699
|
+
forcePendingBlockNumber: opts.forcePendingBlockNumber,
|
|
555
700
|
});
|
|
556
701
|
throw err;
|
|
557
702
|
}
|
|
558
703
|
|
|
559
|
-
this.log.
|
|
704
|
+
this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
|
|
560
705
|
await this.addProposeTx(block, proposeTxArgs, opts, ts);
|
|
561
706
|
return true;
|
|
562
707
|
}
|
|
563
708
|
|
|
709
|
+
public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
|
|
710
|
+
if (!request) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
715
|
+
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
716
|
+
|
|
717
|
+
const logData = { ...pick(request, 'gasUsed', 'blockNumber'), gasLimit, opts };
|
|
718
|
+
this.log.verbose(`Enqueuing invalidate block request`, logData);
|
|
719
|
+
this.addRequest({
|
|
720
|
+
action: `invalidate-by-${request.reason}`,
|
|
721
|
+
request: request.request,
|
|
722
|
+
gasConfig: { gasLimit, txTimeoutAt: opts.txTimeoutAt },
|
|
723
|
+
lastValidL2Slot: this.getCurrentL2Slot() + 2n,
|
|
724
|
+
checkSuccess: (_req, result) => {
|
|
725
|
+
const success =
|
|
726
|
+
result &&
|
|
727
|
+
result.receipt &&
|
|
728
|
+
result.receipt.status === 'success' &&
|
|
729
|
+
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'BlockInvalidated');
|
|
730
|
+
if (!success) {
|
|
731
|
+
this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
|
|
732
|
+
} else {
|
|
733
|
+
this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
|
|
734
|
+
}
|
|
735
|
+
return !!success;
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
564
740
|
/**
|
|
565
741
|
* Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
|
|
566
742
|
* Be warned, the call may return false even if the tx subsequently gets successfully mined.
|
|
@@ -578,7 +754,11 @@ export class SequencerPublisher {
|
|
|
578
754
|
this.l1TxUtils.restart();
|
|
579
755
|
}
|
|
580
756
|
|
|
581
|
-
private async prepareProposeTx(
|
|
757
|
+
private async prepareProposeTx(
|
|
758
|
+
encodedData: L1ProcessArgs,
|
|
759
|
+
timestamp: bigint,
|
|
760
|
+
options: { forcePendingBlockNumber?: number },
|
|
761
|
+
) {
|
|
582
762
|
if (!this.l1TxUtils.client.account) {
|
|
583
763
|
throw new Error('L1 TX utils needs to be initialized with an account wallet.');
|
|
584
764
|
}
|
|
@@ -610,6 +790,11 @@ export class SequencerPublisher {
|
|
|
610
790
|
|
|
611
791
|
const attestations = encodedData.attestations ? encodedData.attestations.map(attest => attest.toViem()) : [];
|
|
612
792
|
const txHashes = encodedData.txHashes ? encodedData.txHashes.map(txHash => txHash.toString()) : [];
|
|
793
|
+
|
|
794
|
+
const signers = encodedData.attestations
|
|
795
|
+
?.filter(attest => !attest.signature.isEmpty())
|
|
796
|
+
.map(attest => attest.address.toString());
|
|
797
|
+
|
|
613
798
|
const args = [
|
|
614
799
|
{
|
|
615
800
|
header: encodedData.header.toViem(),
|
|
@@ -622,10 +807,11 @@ export class SequencerPublisher {
|
|
|
622
807
|
txHashes,
|
|
623
808
|
},
|
|
624
809
|
RollupContract.packAttestations(attestations),
|
|
810
|
+
signers ?? [],
|
|
625
811
|
blobInput,
|
|
626
812
|
] as const;
|
|
627
813
|
|
|
628
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp);
|
|
814
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
|
|
629
815
|
|
|
630
816
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
631
817
|
}
|
|
@@ -648,9 +834,11 @@ export class SequencerPublisher {
|
|
|
648
834
|
};
|
|
649
835
|
},
|
|
650
836
|
ViemCommitteeAttestations,
|
|
837
|
+
`0x${string}`[],
|
|
651
838
|
`0x${string}`,
|
|
652
839
|
],
|
|
653
840
|
timestamp: bigint,
|
|
841
|
+
options: { forcePendingBlockNumber?: number },
|
|
654
842
|
) {
|
|
655
843
|
const rollupData = encodeFunctionData({
|
|
656
844
|
abi: RollupAbi,
|
|
@@ -658,17 +846,18 @@ export class SequencerPublisher {
|
|
|
658
846
|
args,
|
|
659
847
|
});
|
|
660
848
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
849
|
+
// override the pending block number if requested
|
|
850
|
+
const forcePendingBlockNumberStateDiff = (
|
|
851
|
+
options.forcePendingBlockNumber !== undefined
|
|
852
|
+
? await this.rollupContract.makePendingBlockNumberOverride(options.forcePendingBlockNumber)
|
|
853
|
+
: []
|
|
854
|
+
).flatMap(override => override.stateDiff ?? []);
|
|
666
855
|
|
|
667
856
|
const simulationResult = await this.l1TxUtils
|
|
668
857
|
.simulate(
|
|
669
858
|
{
|
|
670
|
-
to:
|
|
671
|
-
data:
|
|
859
|
+
to: this.rollupContract.address,
|
|
860
|
+
data: rollupData,
|
|
672
861
|
gas: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
673
862
|
},
|
|
674
863
|
{
|
|
@@ -682,10 +871,8 @@ export class SequencerPublisher {
|
|
|
682
871
|
address: this.rollupContract.address,
|
|
683
872
|
// @note we override checkBlob to false since blobs are not part simulate()
|
|
684
873
|
stateDiff: [
|
|
685
|
-
{
|
|
686
|
-
|
|
687
|
-
value: toPaddedHex(0n, true),
|
|
688
|
-
},
|
|
874
|
+
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
875
|
+
...forcePendingBlockNumberStateDiff,
|
|
689
876
|
],
|
|
690
877
|
},
|
|
691
878
|
],
|
|
@@ -706,13 +893,28 @@ export class SequencerPublisher {
|
|
|
706
893
|
private async addProposeTx(
|
|
707
894
|
block: L2Block,
|
|
708
895
|
encodedData: L1ProcessArgs,
|
|
709
|
-
opts: { txTimeoutAt?: Date } = {},
|
|
896
|
+
opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
|
|
710
897
|
timestamp: bigint,
|
|
711
898
|
): Promise<void> {
|
|
712
899
|
const timer = new Timer();
|
|
713
900
|
const kzg = Blob.getViemKzgInstance();
|
|
714
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
901
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
902
|
+
encodedData,
|
|
903
|
+
timestamp,
|
|
904
|
+
opts,
|
|
905
|
+
);
|
|
715
906
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
907
|
+
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
908
|
+
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
909
|
+
blobEvaluationGas +
|
|
910
|
+
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
// Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
914
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
|
|
915
|
+
void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch(_err => {
|
|
916
|
+
this.log.error('Failed to send blobs to blob sink');
|
|
917
|
+
});
|
|
716
918
|
|
|
717
919
|
return this.addRequest({
|
|
718
920
|
action: 'propose',
|
|
@@ -721,20 +923,21 @@ export class SequencerPublisher {
|
|
|
721
923
|
data: rollupData,
|
|
722
924
|
},
|
|
723
925
|
lastValidL2Slot: block.header.globalVariables.slotNumber.toBigInt(),
|
|
724
|
-
gasConfig: {
|
|
725
|
-
...opts,
|
|
726
|
-
gasLimit: this.l1TxUtils.bumpGasLimit(simulationResult.gasUsed + blobEvaluationGas),
|
|
727
|
-
},
|
|
926
|
+
gasConfig: { ...opts, gasLimit },
|
|
728
927
|
blobConfig: {
|
|
729
928
|
blobs: encodedData.blobs.map(b => b.data),
|
|
730
929
|
kzg,
|
|
731
930
|
},
|
|
732
|
-
|
|
931
|
+
checkSuccess: (request, result) => {
|
|
733
932
|
if (!result) {
|
|
734
|
-
return;
|
|
933
|
+
return false;
|
|
735
934
|
}
|
|
736
935
|
const { receipt, stats, errorMsg } = result;
|
|
737
|
-
|
|
936
|
+
const success =
|
|
937
|
+
receipt &&
|
|
938
|
+
receipt.status === 'success' &&
|
|
939
|
+
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'L2BlockProposed');
|
|
940
|
+
if (success) {
|
|
738
941
|
const endBlock = receipt.blockNumber;
|
|
739
942
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
740
943
|
const publishStats: L1PublishBlockStats = {
|
|
@@ -749,37 +952,21 @@ export class SequencerPublisher {
|
|
|
749
952
|
blobCount: encodedData.blobs.length,
|
|
750
953
|
inclusionBlocks,
|
|
751
954
|
};
|
|
752
|
-
this.log.
|
|
955
|
+
this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
|
|
753
956
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
754
957
|
|
|
755
|
-
// Send the blobs to the blob sink
|
|
756
|
-
this.sendBlobsToBlobSink(receipt.blockHash, encodedData.blobs).catch(_err => {
|
|
757
|
-
this.log.error('Failed to send blobs to blob sink');
|
|
758
|
-
});
|
|
759
|
-
|
|
760
958
|
return true;
|
|
761
959
|
} else {
|
|
762
960
|
this.metrics.recordFailedTx('process');
|
|
763
|
-
|
|
764
|
-
this.log.error(`Rollup process tx reverted. ${errorMsg ?? 'No error message'}`, undefined, {
|
|
961
|
+
this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
|
|
765
962
|
...block.getStats(),
|
|
963
|
+
receipt,
|
|
766
964
|
txHash: receipt.transactionHash,
|
|
767
965
|
slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
|
|
768
966
|
});
|
|
967
|
+
return false;
|
|
769
968
|
}
|
|
770
969
|
},
|
|
771
970
|
});
|
|
772
971
|
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* Send blobs to the blob sink
|
|
776
|
-
*
|
|
777
|
-
* If a blob sink url is configured, then we send blobs to the blob sink
|
|
778
|
-
* - for now we use the blockHash as the identifier for the blobs;
|
|
779
|
-
* In the future this will move to be the beacon block id - which takes a bit more work
|
|
780
|
-
* to calculate and will need to be mocked in e2e tests
|
|
781
|
-
*/
|
|
782
|
-
protected sendBlobsToBlobSink(blockHash: string, blobs: Blob[]): Promise<boolean> {
|
|
783
|
-
return this.blobSinkClient.sendBlobsToBlobSink(blockHash, blobs);
|
|
784
|
-
}
|
|
785
972
|
}
|