@aztec/sequencer-client 0.0.1-commit.fffb133c → 0.0.1-dev
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 +16 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +56 -23
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +46 -28
- package/dest/global_variable_builder/global_builder.d.ts +14 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +22 -21
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +31 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +101 -42
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +13 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +28 -14
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +137 -74
- package/dest/sequencer/checkpoint_proposal_job.d.ts +35 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +212 -137
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +111 -30
- package/dest/sequencer/sequencer.d.ts +29 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +48 -44
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +12 -12
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +47 -36
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +78 -21
- package/src/config.ts +61 -38
- package/src/global_variable_builder/global_builder.ts +23 -24
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +112 -43
- package/src/publisher/sequencer-publisher-factory.ts +23 -6
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +149 -90
- package/src/sequencer/checkpoint_proposal_job.ts +309 -186
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/metrics.ts +124 -32
- package/src/sequencer/sequencer.ts +61 -50
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +67 -53
- package/src/test/utils.ts +5 -2
|
@@ -4,8 +4,8 @@ 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
|
-
type IEmpireBase,
|
|
9
9
|
MULTI_CALL_3_ADDRESS,
|
|
10
10
|
Multicall3,
|
|
11
11
|
RollupContract,
|
|
@@ -18,14 +18,16 @@ import {
|
|
|
18
18
|
type L1BlobInputs,
|
|
19
19
|
type L1TxConfig,
|
|
20
20
|
type L1TxRequest,
|
|
21
|
+
type L1TxUtils,
|
|
22
|
+
MAX_L1_TX_LIMIT,
|
|
21
23
|
type TransactionStats,
|
|
22
24
|
WEI_CONST,
|
|
23
25
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
24
|
-
import
|
|
25
|
-
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
27
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
28
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
28
29
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
30
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
29
31
|
import { pick } from '@aztec/foundation/collection';
|
|
30
32
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
31
33
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -37,6 +39,7 @@ import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
|
37
39
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
38
40
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
39
41
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
42
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
40
43
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
41
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
42
45
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
@@ -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 {
|
|
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 = [
|
|
@@ -114,6 +119,8 @@ export class SequencerPublisher {
|
|
|
114
119
|
|
|
115
120
|
protected log: Logger;
|
|
116
121
|
protected ethereumSlotDuration: bigint;
|
|
122
|
+
protected aztecSlotDuration: bigint;
|
|
123
|
+
private dateProvider: DateProvider;
|
|
117
124
|
|
|
118
125
|
private blobClient: BlobClientInterface;
|
|
119
126
|
|
|
@@ -122,10 +129,9 @@ export class SequencerPublisher {
|
|
|
122
129
|
|
|
123
130
|
/** L1 fee analyzer for fisherman mode */
|
|
124
131
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
|
|
132
|
+
|
|
133
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
134
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
129
135
|
|
|
130
136
|
// A CALL to a cold address is 2700 gas
|
|
131
137
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -133,7 +139,7 @@ export class SequencerPublisher {
|
|
|
133
139
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
134
140
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
135
141
|
|
|
136
|
-
public l1TxUtils:
|
|
142
|
+
public l1TxUtils: L1TxUtils;
|
|
137
143
|
public rollupContract: RollupContract;
|
|
138
144
|
public govProposerContract: GovernanceProposerContract;
|
|
139
145
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -144,11 +150,12 @@ export class SequencerPublisher {
|
|
|
144
150
|
protected requests: RequestWithExpiry[] = [];
|
|
145
151
|
|
|
146
152
|
constructor(
|
|
147
|
-
private config:
|
|
153
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
|
|
154
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
148
155
|
deps: {
|
|
149
156
|
telemetry?: TelemetryClient;
|
|
150
157
|
blobClient: BlobClientInterface;
|
|
151
|
-
l1TxUtils:
|
|
158
|
+
l1TxUtils: L1TxUtils;
|
|
152
159
|
rollupContract: RollupContract;
|
|
153
160
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
154
161
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -162,6 +169,8 @@ export class SequencerPublisher {
|
|
|
162
169
|
) {
|
|
163
170
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
164
171
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
172
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
173
|
+
this.dateProvider = deps.dateProvider;
|
|
165
174
|
this.epochCache = deps.epochCache;
|
|
166
175
|
this.lastActions = deps.lastActions;
|
|
167
176
|
|
|
@@ -192,12 +201,27 @@ export class SequencerPublisher {
|
|
|
192
201
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
193
202
|
);
|
|
194
203
|
}
|
|
204
|
+
|
|
205
|
+
// Initialize fee asset price oracle
|
|
206
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
207
|
+
this.l1TxUtils.client,
|
|
208
|
+
this.rollupContract,
|
|
209
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
210
|
+
);
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
public getRollupContract(): RollupContract {
|
|
198
214
|
return this.rollupContract;
|
|
199
215
|
}
|
|
200
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Gets the fee asset price modifier from the oracle.
|
|
219
|
+
* Returns 0n if the oracle query fails.
|
|
220
|
+
*/
|
|
221
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
222
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
223
|
+
}
|
|
224
|
+
|
|
201
225
|
public getSenderAddress() {
|
|
202
226
|
return this.l1TxUtils.getSenderAddress();
|
|
203
227
|
}
|
|
@@ -273,7 +297,7 @@ export class SequencerPublisher {
|
|
|
273
297
|
// Start the analysis
|
|
274
298
|
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
275
299
|
l2SlotNumber,
|
|
276
|
-
gasLimit > 0n ? gasLimit :
|
|
300
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
277
301
|
l1Requests,
|
|
278
302
|
blobConfig,
|
|
279
303
|
onComplete,
|
|
@@ -335,8 +359,8 @@ export class SequencerPublisher {
|
|
|
335
359
|
// @note - we can only have one blob config per bundle
|
|
336
360
|
// find requests with gas and blob configs
|
|
337
361
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
338
|
-
const gasConfigs =
|
|
339
|
-
const blobConfigs =
|
|
362
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
363
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
340
364
|
|
|
341
365
|
if (blobConfigs.length > 1) {
|
|
342
366
|
throw new Error('Multiple blob configs found');
|
|
@@ -346,7 +370,16 @@ export class SequencerPublisher {
|
|
|
346
370
|
|
|
347
371
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
348
372
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
349
|
-
|
|
373
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
374
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
375
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
376
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
377
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
378
|
+
requested: gasLimit,
|
|
379
|
+
capped: maxGas,
|
|
380
|
+
});
|
|
381
|
+
gasLimit = maxGas;
|
|
382
|
+
}
|
|
350
383
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
351
384
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
352
385
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -395,7 +428,16 @@ export class SequencerPublisher {
|
|
|
395
428
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
396
429
|
return { failedActions: requests.map(r => r.action) };
|
|
397
430
|
} else {
|
|
398
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
431
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
432
|
+
result,
|
|
433
|
+
requests: requests.map(r => ({
|
|
434
|
+
...r,
|
|
435
|
+
// Avoid logging large blob data
|
|
436
|
+
blobConfig: r.blobConfig
|
|
437
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
438
|
+
: undefined,
|
|
439
|
+
})),
|
|
440
|
+
});
|
|
399
441
|
const successfulActions: Action[] = [];
|
|
400
442
|
const failedActions: Action[] = [];
|
|
401
443
|
for (const request of requests) {
|
|
@@ -410,20 +452,24 @@ export class SequencerPublisher {
|
|
|
410
452
|
}
|
|
411
453
|
|
|
412
454
|
/**
|
|
413
|
-
* @notice Will call `
|
|
455
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
414
456
|
* @param tipArchive - The archive to check
|
|
415
457
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
416
458
|
*/
|
|
417
|
-
public
|
|
459
|
+
public canProposeAt(
|
|
418
460
|
tipArchive: Fr,
|
|
419
461
|
msgSender: EthAddress,
|
|
420
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
462
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
|
|
421
463
|
) {
|
|
422
464
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
423
465
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
424
466
|
|
|
467
|
+
const pipelined = opts.pipelined ?? false;
|
|
468
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
469
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
470
|
+
|
|
425
471
|
return this.rollupContract
|
|
426
|
-
.
|
|
472
|
+
.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
427
473
|
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
428
474
|
})
|
|
429
475
|
.catch(err => {
|
|
@@ -460,7 +506,7 @@ export class SequencerPublisher {
|
|
|
460
506
|
flags,
|
|
461
507
|
] as const;
|
|
462
508
|
|
|
463
|
-
const ts =
|
|
509
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
464
510
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
465
511
|
opts?.forcePendingCheckpointNumber,
|
|
466
512
|
);
|
|
@@ -483,7 +529,7 @@ export class SequencerPublisher {
|
|
|
483
529
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
484
530
|
from: MULTI_CALL_3_ADDRESS,
|
|
485
531
|
},
|
|
486
|
-
{ time: ts
|
|
532
|
+
{ time: ts },
|
|
487
533
|
stateOverrides,
|
|
488
534
|
);
|
|
489
535
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -517,7 +563,12 @@ export class SequencerPublisher {
|
|
|
517
563
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
518
564
|
|
|
519
565
|
try {
|
|
520
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
566
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
567
|
+
request,
|
|
568
|
+
undefined,
|
|
569
|
+
undefined,
|
|
570
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
571
|
+
);
|
|
521
572
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
522
573
|
...logData,
|
|
523
574
|
request,
|
|
@@ -536,7 +587,7 @@ export class SequencerPublisher {
|
|
|
536
587
|
|
|
537
588
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
538
589
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
539
|
-
if (viemError.message?.includes('
|
|
590
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
540
591
|
this.log.verbose(
|
|
541
592
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
542
593
|
{ ...logData, request, error: viemError.message },
|
|
@@ -605,26 +656,9 @@ export class SequencerPublisher {
|
|
|
605
656
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
606
657
|
attestationsAndSignersSignature: Signature,
|
|
607
658
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
608
|
-
): Promise<
|
|
609
|
-
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
|
-
|
|
659
|
+
): Promise<void> {
|
|
626
660
|
const blobFields = checkpoint.toBlobFields();
|
|
627
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
661
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
628
662
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
629
663
|
|
|
630
664
|
const args = [
|
|
@@ -632,7 +666,7 @@ export class SequencerPublisher {
|
|
|
632
666
|
header: checkpoint.header.toViem(),
|
|
633
667
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
634
668
|
oracleInput: {
|
|
635
|
-
feeAssetPriceModifier:
|
|
669
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
636
670
|
},
|
|
637
671
|
},
|
|
638
672
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -641,16 +675,14 @@ export class SequencerPublisher {
|
|
|
641
675
|
blobInput,
|
|
642
676
|
] as const;
|
|
643
677
|
|
|
644
|
-
await this.simulateProposeTx(args,
|
|
645
|
-
return ts;
|
|
678
|
+
await this.simulateProposeTx(args, options);
|
|
646
679
|
}
|
|
647
680
|
|
|
648
681
|
private async enqueueCastSignalHelper(
|
|
649
682
|
slotNumber: SlotNumber,
|
|
650
|
-
timestamp: bigint,
|
|
651
683
|
signalType: GovernanceSignalAction,
|
|
652
684
|
payload: EthAddress,
|
|
653
|
-
base:
|
|
685
|
+
base: GovernanceProposerContract,
|
|
654
686
|
signerAddress: EthAddress,
|
|
655
687
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
656
688
|
): Promise<boolean> {
|
|
@@ -681,6 +713,26 @@ export class SequencerPublisher {
|
|
|
681
713
|
return false;
|
|
682
714
|
}
|
|
683
715
|
|
|
716
|
+
// Skip signaling if there is already a live (non-terminal) Governance proposal for this
|
|
717
|
+
// payload. This is intentionally not cached: a previously-live proposal may transition to
|
|
718
|
+
// a terminal state (Dropped/Rejected/Expired/Executed), at which point we may want to re-signal
|
|
719
|
+
// the same payload in a future round.
|
|
720
|
+
let proposed = false;
|
|
721
|
+
try {
|
|
722
|
+
proposed = await base.hasActiveProposalWithPayload(payload.toString());
|
|
723
|
+
} catch (err) {
|
|
724
|
+
// We deliberately swallow the error and proceed to signal. Failing closed (skipping the
|
|
725
|
+
// signal) on transient RPC errors would let a flaky L1 endpoint silence governance
|
|
726
|
+
// participation entirely; failing open at worst produces a duplicate signal that the
|
|
727
|
+
// contract will simply count alongside others in the round.
|
|
728
|
+
this.log.error(`Failed to check if payload ${payload} was already proposed (signalling anyway)`, err);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (proposed) {
|
|
732
|
+
this.log.info(`Payload ${payload} has a live governance proposal, stopping signals`);
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
|
|
684
736
|
const cachedLastVote = this.lastActions[signalType];
|
|
685
737
|
this.lastActions[signalType] = slotNumber;
|
|
686
738
|
const action = signalType;
|
|
@@ -699,11 +751,16 @@ export class SequencerPublisher {
|
|
|
699
751
|
lastValidL2Slot: slotNumber,
|
|
700
752
|
});
|
|
701
753
|
|
|
754
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
755
|
+
|
|
702
756
|
try {
|
|
703
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
757
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
704
758
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
705
759
|
} catch (err) {
|
|
706
|
-
|
|
760
|
+
const viemError = formatViemError(err);
|
|
761
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
762
|
+
simulationTimestamp: timestamp,
|
|
763
|
+
});
|
|
707
764
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
708
765
|
}
|
|
709
766
|
|
|
@@ -754,19 +811,16 @@ export class SequencerPublisher {
|
|
|
754
811
|
/**
|
|
755
812
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
756
813
|
* @param slotNumber - The slot number to cast a signal for.
|
|
757
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
758
814
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
759
815
|
*/
|
|
760
816
|
public enqueueGovernanceCastSignal(
|
|
761
817
|
governancePayload: EthAddress,
|
|
762
818
|
slotNumber: SlotNumber,
|
|
763
|
-
timestamp: bigint,
|
|
764
819
|
signerAddress: EthAddress,
|
|
765
820
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
766
821
|
): Promise<boolean> {
|
|
767
822
|
return this.enqueueCastSignalHelper(
|
|
768
823
|
slotNumber,
|
|
769
|
-
timestamp,
|
|
770
824
|
'governance-signal',
|
|
771
825
|
governancePayload,
|
|
772
826
|
this.govProposerContract,
|
|
@@ -779,7 +833,6 @@ export class SequencerPublisher {
|
|
|
779
833
|
public async enqueueSlashingActions(
|
|
780
834
|
actions: ProposerSlashAction[],
|
|
781
835
|
slotNumber: SlotNumber,
|
|
782
|
-
timestamp: bigint,
|
|
783
836
|
signerAddress: EthAddress,
|
|
784
837
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
785
838
|
): Promise<boolean> {
|
|
@@ -800,10 +853,9 @@ export class SequencerPublisher {
|
|
|
800
853
|
});
|
|
801
854
|
await this.enqueueCastSignalHelper(
|
|
802
855
|
slotNumber,
|
|
803
|
-
timestamp,
|
|
804
856
|
'empire-slashing-signal',
|
|
805
857
|
action.payload,
|
|
806
|
-
this.slashingProposerContract,
|
|
858
|
+
this.slashingProposerContract as any,
|
|
807
859
|
signerAddress,
|
|
808
860
|
signer,
|
|
809
861
|
);
|
|
@@ -819,7 +871,6 @@ export class SequencerPublisher {
|
|
|
819
871
|
(receipt: TransactionReceipt) =>
|
|
820
872
|
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
821
873
|
slotNumber,
|
|
822
|
-
timestamp,
|
|
823
874
|
);
|
|
824
875
|
break;
|
|
825
876
|
}
|
|
@@ -837,7 +888,6 @@ export class SequencerPublisher {
|
|
|
837
888
|
request,
|
|
838
889
|
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
839
890
|
slotNumber,
|
|
840
|
-
timestamp,
|
|
841
891
|
);
|
|
842
892
|
break;
|
|
843
893
|
}
|
|
@@ -861,7 +911,6 @@ export class SequencerPublisher {
|
|
|
861
911
|
request,
|
|
862
912
|
(receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
|
|
863
913
|
slotNumber,
|
|
864
|
-
timestamp,
|
|
865
914
|
);
|
|
866
915
|
break;
|
|
867
916
|
}
|
|
@@ -883,7 +932,6 @@ export class SequencerPublisher {
|
|
|
883
932
|
request,
|
|
884
933
|
(receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
|
|
885
934
|
slotNumber,
|
|
886
|
-
timestamp,
|
|
887
935
|
);
|
|
888
936
|
break;
|
|
889
937
|
}
|
|
@@ -908,25 +956,24 @@ export class SequencerPublisher {
|
|
|
908
956
|
const checkpointHeader = checkpoint.header;
|
|
909
957
|
|
|
910
958
|
const blobFields = checkpoint.toBlobFields();
|
|
911
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
959
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
912
960
|
|
|
913
|
-
const proposeTxArgs = {
|
|
961
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
914
962
|
header: checkpointHeader,
|
|
915
963
|
archive: checkpoint.archive.root.toBuffer(),
|
|
916
964
|
blobs,
|
|
917
965
|
attestationsAndSigners,
|
|
918
966
|
attestationsAndSignersSignature,
|
|
967
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
919
968
|
};
|
|
920
969
|
|
|
921
|
-
let ts: bigint;
|
|
922
|
-
|
|
923
970
|
try {
|
|
924
971
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
925
972
|
// This means that we can avoid the simulation issues in later checks.
|
|
926
973
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
927
974
|
// make time consistency checks break.
|
|
928
975
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
929
|
-
|
|
976
|
+
await this.validateCheckpointForSubmission(
|
|
930
977
|
checkpoint,
|
|
931
978
|
attestationsAndSigners,
|
|
932
979
|
attestationsAndSignersSignature,
|
|
@@ -942,7 +989,7 @@ export class SequencerPublisher {
|
|
|
942
989
|
}
|
|
943
990
|
|
|
944
991
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
945
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts
|
|
992
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts);
|
|
946
993
|
}
|
|
947
994
|
|
|
948
995
|
public enqueueInvalidateCheckpoint(
|
|
@@ -985,8 +1032,8 @@ export class SequencerPublisher {
|
|
|
985
1032
|
request: L1TxRequest,
|
|
986
1033
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
987
1034
|
slotNumber: SlotNumber,
|
|
988
|
-
timestamp: bigint,
|
|
989
1035
|
) {
|
|
1036
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
990
1037
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
991
1038
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
992
1039
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
@@ -999,12 +1046,15 @@ export class SequencerPublisher {
|
|
|
999
1046
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1000
1047
|
|
|
1001
1048
|
let gasUsed: bigint;
|
|
1049
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1050
|
+
|
|
1002
1051
|
try {
|
|
1003
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1052
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1004
1053
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1005
1054
|
} catch (err) {
|
|
1006
|
-
const viemError = formatViemError(err);
|
|
1055
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1007
1056
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1057
|
+
|
|
1008
1058
|
return false;
|
|
1009
1059
|
}
|
|
1010
1060
|
|
|
@@ -1012,10 +1062,14 @@ export class SequencerPublisher {
|
|
|
1012
1062
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1013
1063
|
logData.gasLimit = gasLimit;
|
|
1014
1064
|
|
|
1065
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1066
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1067
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1068
|
+
|
|
1015
1069
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1016
1070
|
this.addRequest({
|
|
1017
1071
|
action,
|
|
1018
|
-
request,
|
|
1072
|
+
request: requestWithAbi,
|
|
1019
1073
|
gasConfig: { gasLimit },
|
|
1020
1074
|
lastValidL2Slot: slotNumber,
|
|
1021
1075
|
checkSuccess: (_req, result) => {
|
|
@@ -1051,7 +1105,6 @@ export class SequencerPublisher {
|
|
|
1051
1105
|
|
|
1052
1106
|
private async prepareProposeTx(
|
|
1053
1107
|
encodedData: L1ProcessArgs,
|
|
1054
|
-
timestamp: bigint,
|
|
1055
1108
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1056
1109
|
) {
|
|
1057
1110
|
const kzg = Blob.getViemKzgInstance();
|
|
@@ -1097,8 +1150,7 @@ export class SequencerPublisher {
|
|
|
1097
1150
|
header: encodedData.header.toViem(),
|
|
1098
1151
|
archive: toHex(encodedData.archive),
|
|
1099
1152
|
oracleInput: {
|
|
1100
|
-
|
|
1101
|
-
feeAssetPriceModifier: 0n,
|
|
1153
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1102
1154
|
},
|
|
1103
1155
|
},
|
|
1104
1156
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1107,7 +1159,7 @@ export class SequencerPublisher {
|
|
|
1107
1159
|
blobInput,
|
|
1108
1160
|
] as const;
|
|
1109
1161
|
|
|
1110
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1162
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
|
|
1111
1163
|
|
|
1112
1164
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1113
1165
|
}
|
|
@@ -1115,7 +1167,6 @@ export class SequencerPublisher {
|
|
|
1115
1167
|
/**
|
|
1116
1168
|
* Simulates the propose tx with eth_simulateV1
|
|
1117
1169
|
* @param args - The propose tx args
|
|
1118
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1119
1170
|
* @returns The simulation result
|
|
1120
1171
|
*/
|
|
1121
1172
|
private async simulateProposeTx(
|
|
@@ -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:
|
|
1178
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1128
1179
|
};
|
|
1129
1180
|
},
|
|
1130
1181
|
ViemCommitteeAttestations,
|
|
@@ -1132,7 +1183,6 @@ export class SequencerPublisher {
|
|
|
1132
1183
|
ViemSignature,
|
|
1133
1184
|
`0x${string}`,
|
|
1134
1185
|
],
|
|
1135
|
-
timestamp: bigint,
|
|
1136
1186
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1137
1187
|
) {
|
|
1138
1188
|
const rollupData = encodeFunctionData({
|
|
@@ -1166,25 +1216,26 @@ export class SequencerPublisher {
|
|
|
1166
1216
|
});
|
|
1167
1217
|
}
|
|
1168
1218
|
|
|
1219
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1220
|
+
|
|
1169
1221
|
const simulationResult = await this.l1TxUtils
|
|
1170
1222
|
.simulate(
|
|
1171
1223
|
{
|
|
1172
1224
|
to: this.rollupContract.address,
|
|
1173
1225
|
data: rollupData,
|
|
1174
|
-
gas:
|
|
1226
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1175
1227
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1176
1228
|
},
|
|
1177
1229
|
{
|
|
1178
|
-
|
|
1179
|
-
time: timestamp + 1n,
|
|
1230
|
+
time: simTs,
|
|
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:
|
|
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:
|
|
1238
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1188
1239
|
},
|
|
1189
1240
|
)
|
|
1190
1241
|
.catch(err => {
|
|
@@ -1194,11 +1245,11 @@ 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:
|
|
1248
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1198
1249
|
logs: [],
|
|
1199
1250
|
};
|
|
1200
1251
|
}
|
|
1201
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1252
|
+
this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
|
|
1202
1253
|
throw err;
|
|
1203
1254
|
});
|
|
1204
1255
|
|
|
@@ -1209,16 +1260,11 @@ export class SequencerPublisher {
|
|
|
1209
1260
|
checkpoint: Checkpoint,
|
|
1210
1261
|
encodedData: L1ProcessArgs,
|
|
1211
1262
|
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1212
|
-
timestamp: bigint,
|
|
1213
1263
|
): Promise<void> {
|
|
1214
1264
|
const slot = checkpoint.header.slotNumber;
|
|
1215
1265
|
const timer = new Timer();
|
|
1216
1266
|
const kzg = Blob.getViemKzgInstance();
|
|
1217
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1218
|
-
encodedData,
|
|
1219
|
-
timestamp,
|
|
1220
|
-
opts,
|
|
1221
|
-
);
|
|
1267
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
|
|
1222
1268
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1223
1269
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
1224
1270
|
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
@@ -1294,4 +1340,17 @@ export class SequencerPublisher {
|
|
|
1294
1340
|
},
|
|
1295
1341
|
});
|
|
1296
1342
|
}
|
|
1343
|
+
|
|
1344
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1345
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
|
|
1346
|
+
private getSimulationTimestamp(slot: SlotNumber): bigint {
|
|
1347
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1348
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1352
|
+
private getNextL1SlotTimestamp(): bigint {
|
|
1353
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1354
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1355
|
+
}
|
|
1297
1356
|
}
|