@aztec/sequencer-client 0.0.1-commit.993d52e → 0.0.1-commit.9badcec54
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 +6 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +49 -27
- package/dest/config.d.ts +25 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +27 -21
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +128 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +15 -14
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +16 -50
- package/dest/global_variable_builder/index.d.ts +4 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/publisher/config.d.ts +17 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +25 -5
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +16 -3
- package/dest/publisher/sequencer-publisher.d.ts +61 -43
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +392 -119
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +26 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +337 -209
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- 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/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +9 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +23 -20
- package/dest/sequencer/sequencer.d.ts +27 -11
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +137 -74
- package/dest/sequencer/timetable.d.ts +17 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -43
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +7 -9
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +39 -30
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +61 -28
- package/src/config.ts +35 -24
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +26 -62
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +47 -4
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +18 -6
- package/src/publisher/sequencer-publisher.ts +437 -171
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +422 -237
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +29 -25
- package/src/sequencer/sequencer.ts +198 -81
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +51 -48
|
@@ -3,16 +3,18 @@ import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/
|
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
|
-
type EmpireSlashingProposerContract,
|
|
7
6
|
FeeAssetPriceOracle,
|
|
8
7
|
type GovernanceProposerContract,
|
|
9
8
|
type IEmpireBase,
|
|
10
9
|
MULTI_CALL_3_ADDRESS,
|
|
11
10
|
Multicall3,
|
|
12
11
|
RollupContract,
|
|
13
|
-
|
|
12
|
+
SimulationOverridesBuilder,
|
|
13
|
+
type SimulationOverridesPlan,
|
|
14
|
+
type SlashingProposerContract,
|
|
14
15
|
type ViemCommitteeAttestations,
|
|
15
16
|
type ViemHeader,
|
|
17
|
+
buildSimulationOverridesStateOverride,
|
|
16
18
|
} from '@aztec/ethereum/contracts';
|
|
17
19
|
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
18
20
|
import {
|
|
@@ -26,30 +28,55 @@ import {
|
|
|
26
28
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
27
29
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
28
30
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
29
|
-
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
30
31
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
32
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
31
33
|
import { pick } from '@aztec/foundation/collection';
|
|
32
34
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
33
36
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
34
37
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
35
38
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
36
39
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
37
41
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
38
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
42
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
39
43
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
40
44
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
41
45
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
42
46
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
43
|
-
import {
|
|
47
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
44
48
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
45
49
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
46
50
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
47
51
|
|
|
48
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
type Hex,
|
|
54
|
+
type TransactionReceipt,
|
|
55
|
+
type TypedDataDefinition,
|
|
56
|
+
encodeFunctionData,
|
|
57
|
+
keccak256,
|
|
58
|
+
multicall3Abi,
|
|
59
|
+
toHex,
|
|
60
|
+
} from 'viem';
|
|
49
61
|
|
|
50
62
|
import type { SequencerPublisherConfig } from './config.js';
|
|
63
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
51
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
52
65
|
|
|
66
|
+
/** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
|
|
67
|
+
export type SendRequestsResult = {
|
|
68
|
+
/** The L1 transaction receipt or error from the bundled multicall. */
|
|
69
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
|
|
70
|
+
/** Actions that expired (past their deadline) before the request was sent. */
|
|
71
|
+
expiredActions: Action[];
|
|
72
|
+
/** Actions that were included in the sent L1 transaction. */
|
|
73
|
+
sentActions: Action[];
|
|
74
|
+
/** Actions whose L1 simulation succeeded (subset of sentActions). */
|
|
75
|
+
successfulActions: Action[];
|
|
76
|
+
/** Actions whose L1 simulation failed (subset of sentActions). */
|
|
77
|
+
failedActions: Action[];
|
|
78
|
+
};
|
|
79
|
+
|
|
53
80
|
/** Arguments to the process method of the rollup contract */
|
|
54
81
|
type L1ProcessArgs = {
|
|
55
82
|
/** The L2 block header. */
|
|
@@ -71,16 +98,13 @@ export const Actions = [
|
|
|
71
98
|
'invalidate-by-insufficient-attestations',
|
|
72
99
|
'propose',
|
|
73
100
|
'governance-signal',
|
|
74
|
-
'empire-slashing-signal',
|
|
75
|
-
'create-empire-payload',
|
|
76
|
-
'execute-empire-payload',
|
|
77
101
|
'vote-offenses',
|
|
78
102
|
'execute-slash',
|
|
79
103
|
] as const;
|
|
80
104
|
|
|
81
105
|
export type Action = (typeof Actions)[number];
|
|
82
106
|
|
|
83
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
107
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
84
108
|
|
|
85
109
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
86
110
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -91,6 +115,13 @@ export type InvalidateCheckpointRequest = {
|
|
|
91
115
|
gasUsed: bigint;
|
|
92
116
|
checkpointNumber: CheckpointNumber;
|
|
93
117
|
forcePendingCheckpointNumber: CheckpointNumber;
|
|
118
|
+
/** Archive at the rollback target checkpoint (checkpoint N-1). */
|
|
119
|
+
lastArchive: Fr;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type EnqueueProposeCheckpointOpts = {
|
|
123
|
+
txTimeoutAt?: Date;
|
|
124
|
+
simulationOverridesPlan?: SimulationOverridesPlan;
|
|
94
125
|
};
|
|
95
126
|
|
|
96
127
|
interface RequestWithExpiry {
|
|
@@ -99,6 +130,8 @@ interface RequestWithExpiry {
|
|
|
99
130
|
lastValidL2Slot: SlotNumber;
|
|
100
131
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
101
132
|
blobConfig?: L1BlobInputs;
|
|
133
|
+
/** Optional pre-send validation. If it rejects, the request is discarded. */
|
|
134
|
+
preCheck?: () => Promise<void>;
|
|
102
135
|
checkSuccess: (
|
|
103
136
|
request: L1TxRequest,
|
|
104
137
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -109,6 +142,7 @@ export class SequencerPublisher {
|
|
|
109
142
|
private interrupted = false;
|
|
110
143
|
private metrics: SequencerPublisherMetrics;
|
|
111
144
|
public epochCache: EpochCache;
|
|
145
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
112
146
|
|
|
113
147
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
114
148
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -120,18 +154,28 @@ export class SequencerPublisher {
|
|
|
120
154
|
|
|
121
155
|
protected log: Logger;
|
|
122
156
|
protected ethereumSlotDuration: bigint;
|
|
157
|
+
protected aztecSlotDuration: bigint;
|
|
158
|
+
|
|
159
|
+
/** Date provider for wall-clock time. */
|
|
160
|
+
private readonly dateProvider: DateProvider;
|
|
123
161
|
|
|
124
162
|
private blobClient: BlobClientInterface;
|
|
125
163
|
|
|
126
164
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
127
165
|
private proposerAddressForSimulation?: EthAddress;
|
|
128
166
|
|
|
167
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
168
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
169
|
+
|
|
129
170
|
/** L1 fee analyzer for fisherman mode */
|
|
130
171
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
131
172
|
|
|
132
173
|
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
133
174
|
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
134
175
|
|
|
176
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
177
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
178
|
+
|
|
135
179
|
// A CALL to a cold address is 2700 gas
|
|
136
180
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
137
181
|
|
|
@@ -141,42 +185,45 @@ export class SequencerPublisher {
|
|
|
141
185
|
public l1TxUtils: L1TxUtils;
|
|
142
186
|
public rollupContract: RollupContract;
|
|
143
187
|
public govProposerContract: GovernanceProposerContract;
|
|
144
|
-
public slashingProposerContract:
|
|
145
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
188
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
146
189
|
|
|
147
190
|
public readonly tracer: Tracer;
|
|
148
191
|
|
|
149
192
|
protected requests: RequestWithExpiry[] = [];
|
|
150
193
|
|
|
151
194
|
constructor(
|
|
152
|
-
private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
|
|
153
|
-
Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
|
|
195
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
196
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
154
197
|
deps: {
|
|
155
198
|
telemetry?: TelemetryClient;
|
|
156
199
|
blobClient: BlobClientInterface;
|
|
157
200
|
l1TxUtils: L1TxUtils;
|
|
158
201
|
rollupContract: RollupContract;
|
|
159
|
-
slashingProposerContract:
|
|
202
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
160
203
|
governanceProposerContract: GovernanceProposerContract;
|
|
161
|
-
slashFactoryContract: SlashFactoryContract;
|
|
162
204
|
epochCache: EpochCache;
|
|
163
205
|
dateProvider: DateProvider;
|
|
164
206
|
metrics: SequencerPublisherMetrics;
|
|
165
207
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
166
208
|
log?: Logger;
|
|
209
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
167
210
|
},
|
|
168
211
|
) {
|
|
169
212
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
170
213
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
214
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
215
|
+
this.dateProvider = deps.dateProvider;
|
|
171
216
|
this.epochCache = deps.epochCache;
|
|
172
217
|
this.lastActions = deps.lastActions;
|
|
173
218
|
|
|
174
219
|
this.blobClient = deps.blobClient;
|
|
220
|
+
this.dateProvider = deps.dateProvider;
|
|
175
221
|
|
|
176
222
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
177
223
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
178
224
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
179
225
|
this.l1TxUtils = deps.l1TxUtils;
|
|
226
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
180
227
|
|
|
181
228
|
this.rollupContract = deps.rollupContract;
|
|
182
229
|
|
|
@@ -188,8 +235,6 @@ export class SequencerPublisher {
|
|
|
188
235
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
189
236
|
this.slashingProposerContract = newSlashingProposer;
|
|
190
237
|
});
|
|
191
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
192
|
-
|
|
193
238
|
// Initialize L1 fee analyzer for fisherman mode
|
|
194
239
|
if (config.fishermanMode) {
|
|
195
240
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
@@ -205,6 +250,31 @@ export class SequencerPublisher {
|
|
|
205
250
|
this.rollupContract,
|
|
206
251
|
createLogger('sequencer:publisher:price-oracle'),
|
|
207
252
|
);
|
|
253
|
+
|
|
254
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
255
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
260
|
+
* Does nothing if no store is configured.
|
|
261
|
+
*/
|
|
262
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
263
|
+
if (!this.failedTxStore) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tx: FailedL1Tx = {
|
|
268
|
+
...failedTx,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Fire and forget - don't block on backup
|
|
273
|
+
void this.failedTxStore
|
|
274
|
+
.then(store => store?.saveFailedTx(tx))
|
|
275
|
+
.catch(err => {
|
|
276
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
277
|
+
});
|
|
208
278
|
}
|
|
209
279
|
|
|
210
280
|
public getRollupContract(): RollupContract {
|
|
@@ -243,7 +313,7 @@ export class SequencerPublisher {
|
|
|
243
313
|
}
|
|
244
314
|
|
|
245
315
|
public getCurrentL2Slot(): SlotNumber {
|
|
246
|
-
return this.epochCache.
|
|
316
|
+
return this.epochCache.getSlotNow();
|
|
247
317
|
}
|
|
248
318
|
|
|
249
319
|
/**
|
|
@@ -321,9 +391,10 @@ export class SequencerPublisher {
|
|
|
321
391
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
322
392
|
*/
|
|
323
393
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
324
|
-
public async sendRequests() {
|
|
394
|
+
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
325
395
|
const requestsToProcess = [...this.requests];
|
|
326
396
|
this.requests = [];
|
|
397
|
+
|
|
327
398
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
328
399
|
return undefined;
|
|
329
400
|
}
|
|
@@ -356,8 +427,8 @@ export class SequencerPublisher {
|
|
|
356
427
|
// @note - we can only have one blob config per bundle
|
|
357
428
|
// find requests with gas and blob configs
|
|
358
429
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
359
|
-
const gasConfigs =
|
|
360
|
-
const blobConfigs =
|
|
430
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
431
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
361
432
|
|
|
362
433
|
if (blobConfigs.length > 1) {
|
|
363
434
|
throw new Error('Multiple blob configs found');
|
|
@@ -386,19 +457,36 @@ export class SequencerPublisher {
|
|
|
386
457
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
387
458
|
|
|
388
459
|
try {
|
|
460
|
+
// Capture context for failed tx backup before sending
|
|
461
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
462
|
+
const multicallData = encodeFunctionData({
|
|
463
|
+
abi: multicall3Abi,
|
|
464
|
+
functionName: 'aggregate3',
|
|
465
|
+
args: [
|
|
466
|
+
validRequests.map(r => ({
|
|
467
|
+
target: r.request.to!,
|
|
468
|
+
callData: r.request.data!,
|
|
469
|
+
allowFailure: true,
|
|
470
|
+
})),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
474
|
+
|
|
475
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
476
|
+
|
|
389
477
|
this.log.debug('Forwarding transactions', {
|
|
390
478
|
validRequests: validRequests.map(request => request.action),
|
|
391
479
|
txConfig,
|
|
392
480
|
});
|
|
393
|
-
const result = await
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
481
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
482
|
+
if (result === undefined) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
486
|
+
validRequests,
|
|
487
|
+
result,
|
|
488
|
+
txContext,
|
|
400
489
|
);
|
|
401
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
402
490
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
403
491
|
} catch (err) {
|
|
404
492
|
const viemError = formatViemError(err);
|
|
@@ -416,16 +504,127 @@ export class SequencerPublisher {
|
|
|
416
504
|
}
|
|
417
505
|
}
|
|
418
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
509
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
510
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
511
|
+
*/
|
|
512
|
+
private async forwardWithPublisherRotation(
|
|
513
|
+
validRequests: RequestWithExpiry[],
|
|
514
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
515
|
+
blobConfig: L1BlobInputs | undefined,
|
|
516
|
+
) {
|
|
517
|
+
const triedAddresses: EthAddress[] = [];
|
|
518
|
+
let currentPublisher = this.l1TxUtils;
|
|
519
|
+
|
|
520
|
+
while (true) {
|
|
521
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
522
|
+
try {
|
|
523
|
+
const result = await Multicall3.forward(
|
|
524
|
+
validRequests.map(r => r.request),
|
|
525
|
+
currentPublisher,
|
|
526
|
+
txConfig,
|
|
527
|
+
blobConfig,
|
|
528
|
+
this.rollupContract.address,
|
|
529
|
+
this.log,
|
|
530
|
+
);
|
|
531
|
+
this.l1TxUtils = currentPublisher;
|
|
532
|
+
return result;
|
|
533
|
+
} catch (err) {
|
|
534
|
+
if (err instanceof TimeoutError) {
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
537
|
+
const viemError = formatViemError(err);
|
|
538
|
+
if (!this.getNextPublisher) {
|
|
539
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
this.log.warn(
|
|
543
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
544
|
+
viemError,
|
|
545
|
+
);
|
|
546
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
547
|
+
if (!nextPublisher) {
|
|
548
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
currentPublisher = nextPublisher;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/*
|
|
557
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
558
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
559
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
560
|
+
*/
|
|
561
|
+
public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
|
|
562
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
563
|
+
if (ms > 0) {
|
|
564
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
|
|
565
|
+
await this.interruptibleSleep.sleep(ms);
|
|
566
|
+
}
|
|
567
|
+
if (this.interrupted) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
|
|
572
|
+
const validRequests: RequestWithExpiry[] = [];
|
|
573
|
+
for (const request of this.requests) {
|
|
574
|
+
if (!request.preCheck) {
|
|
575
|
+
validRequests.push(request);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
await request.preCheck();
|
|
581
|
+
validRequests.push(request);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.requests = validRequests;
|
|
588
|
+
if (this.requests.length === 0) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return this.sendRequests();
|
|
593
|
+
}
|
|
594
|
+
|
|
419
595
|
private callbackBundledTransactions(
|
|
420
596
|
requests: RequestWithExpiry[],
|
|
421
|
-
result
|
|
597
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
598
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
422
599
|
) {
|
|
423
600
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
424
601
|
if (result instanceof FormattedViemError) {
|
|
425
602
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
603
|
+
this.backupFailedTx({
|
|
604
|
+
id: keccak256(txContext.multicallData),
|
|
605
|
+
failureType: 'send-error',
|
|
606
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
607
|
+
blobData: txContext.blobData,
|
|
608
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
609
|
+
error: { message: result.message, name: result.name },
|
|
610
|
+
context: {
|
|
611
|
+
actions: requests.map(r => r.action),
|
|
612
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
613
|
+
sender: this.getSenderAddress().toString(),
|
|
614
|
+
},
|
|
615
|
+
});
|
|
426
616
|
return { failedActions: requests.map(r => r.action) };
|
|
427
617
|
} else {
|
|
428
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
618
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
619
|
+
result,
|
|
620
|
+
requests: requests.map(r => ({
|
|
621
|
+
...r,
|
|
622
|
+
// Avoid logging large blob data
|
|
623
|
+
blobConfig: r.blobConfig
|
|
624
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
625
|
+
: undefined,
|
|
626
|
+
})),
|
|
627
|
+
});
|
|
429
628
|
const successfulActions: Action[] = [];
|
|
430
629
|
const failedActions: Action[] = [];
|
|
431
630
|
for (const request of requests) {
|
|
@@ -435,27 +634,54 @@ export class SequencerPublisher {
|
|
|
435
634
|
failedActions.push(request.action);
|
|
436
635
|
}
|
|
437
636
|
}
|
|
637
|
+
// Single backup for the whole reverted tx
|
|
638
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
639
|
+
this.backupFailedTx({
|
|
640
|
+
id: result.receipt.transactionHash,
|
|
641
|
+
failureType: 'revert',
|
|
642
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
643
|
+
blobData: txContext.blobData,
|
|
644
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
645
|
+
receipt: {
|
|
646
|
+
transactionHash: result.receipt.transactionHash,
|
|
647
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
648
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
649
|
+
status: 'reverted',
|
|
650
|
+
},
|
|
651
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
652
|
+
context: {
|
|
653
|
+
actions: failedActions,
|
|
654
|
+
requests: requests
|
|
655
|
+
.filter(r => failedActions.includes(r.action))
|
|
656
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
657
|
+
sender: this.getSenderAddress().toString(),
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
438
661
|
return { successfulActions, failedActions };
|
|
439
662
|
}
|
|
440
663
|
}
|
|
441
664
|
|
|
442
665
|
/**
|
|
443
|
-
* @notice Will call `
|
|
666
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
444
667
|
* @param tipArchive - The archive to check
|
|
445
668
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
446
669
|
*/
|
|
447
|
-
public
|
|
448
|
-
tipArchive: Fr,
|
|
449
|
-
msgSender: EthAddress,
|
|
450
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
451
|
-
) {
|
|
670
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
452
671
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
453
672
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
454
673
|
|
|
674
|
+
const pipelined = this.epochCache.isProposerPipeliningEnabled();
|
|
675
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
676
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
677
|
+
|
|
455
678
|
return this.rollupContract
|
|
456
|
-
.
|
|
457
|
-
|
|
458
|
-
|
|
679
|
+
.canProposeAt(
|
|
680
|
+
tipArchive.toBuffer(),
|
|
681
|
+
msgSender.toString(),
|
|
682
|
+
nextL1SlotTs,
|
|
683
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
684
|
+
)
|
|
459
685
|
.catch(err => {
|
|
460
686
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
461
687
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -467,6 +693,7 @@ export class SequencerPublisher {
|
|
|
467
693
|
return undefined;
|
|
468
694
|
});
|
|
469
695
|
}
|
|
696
|
+
|
|
470
697
|
/**
|
|
471
698
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
472
699
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
@@ -476,7 +703,7 @@ export class SequencerPublisher {
|
|
|
476
703
|
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
477
704
|
public async validateBlockHeader(
|
|
478
705
|
header: CheckpointHeader,
|
|
479
|
-
|
|
706
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
480
707
|
): Promise<void> {
|
|
481
708
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
482
709
|
|
|
@@ -490,10 +717,8 @@ export class SequencerPublisher {
|
|
|
490
717
|
flags,
|
|
491
718
|
] as const;
|
|
492
719
|
|
|
493
|
-
const ts =
|
|
494
|
-
const stateOverrides = await this.rollupContract
|
|
495
|
-
opts?.forcePendingCheckpointNumber,
|
|
496
|
-
);
|
|
720
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
721
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
497
722
|
let balance = 0n;
|
|
498
723
|
if (this.config.fishermanMode) {
|
|
499
724
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -513,7 +738,7 @@ export class SequencerPublisher {
|
|
|
513
738
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
514
739
|
from: MULTI_CALL_3_ADDRESS,
|
|
515
740
|
},
|
|
516
|
-
{ time: ts
|
|
741
|
+
{ time: ts },
|
|
517
742
|
stateOverrides,
|
|
518
743
|
);
|
|
519
744
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -546,6 +771,8 @@ export class SequencerPublisher {
|
|
|
546
771
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
547
772
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
548
773
|
|
|
774
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
775
|
+
|
|
549
776
|
try {
|
|
550
777
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
551
778
|
request,
|
|
@@ -564,6 +791,7 @@ export class SequencerPublisher {
|
|
|
564
791
|
gasUsed,
|
|
565
792
|
checkpointNumber,
|
|
566
793
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
794
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
567
795
|
reason,
|
|
568
796
|
};
|
|
569
797
|
} catch (err) {
|
|
@@ -576,8 +804,8 @@ export class SequencerPublisher {
|
|
|
576
804
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
577
805
|
{ ...logData, request, error: viemError.message },
|
|
578
806
|
);
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
807
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
808
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
581
809
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
582
810
|
return undefined;
|
|
583
811
|
} else {
|
|
@@ -597,6 +825,18 @@ export class SequencerPublisher {
|
|
|
597
825
|
|
|
598
826
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
599
827
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
828
|
+
this.backupFailedTx({
|
|
829
|
+
id: keccak256(request.data!),
|
|
830
|
+
failureType: 'simulation',
|
|
831
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
832
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
833
|
+
error: { message: viemError.message, name: viemError.name },
|
|
834
|
+
context: {
|
|
835
|
+
actions: [`invalidate-${reason}`],
|
|
836
|
+
checkpointNumber,
|
|
837
|
+
sender: this.getSenderAddress().toString(),
|
|
838
|
+
},
|
|
839
|
+
});
|
|
600
840
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
601
841
|
}
|
|
602
842
|
}
|
|
@@ -639,9 +879,8 @@ export class SequencerPublisher {
|
|
|
639
879
|
checkpoint: Checkpoint,
|
|
640
880
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
641
881
|
attestationsAndSignersSignature: Signature,
|
|
642
|
-
|
|
643
|
-
): Promise<
|
|
644
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
882
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
883
|
+
): Promise<void> {
|
|
645
884
|
const blobFields = checkpoint.toBlobFields();
|
|
646
885
|
const blobs = await getBlobsPerL1Block(blobFields);
|
|
647
886
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
@@ -660,13 +899,11 @@ export class SequencerPublisher {
|
|
|
660
899
|
blobInput,
|
|
661
900
|
] as const;
|
|
662
901
|
|
|
663
|
-
await this.simulateProposeTx(args,
|
|
664
|
-
return ts;
|
|
902
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
665
903
|
}
|
|
666
904
|
|
|
667
905
|
private async enqueueCastSignalHelper(
|
|
668
906
|
slotNumber: SlotNumber,
|
|
669
|
-
timestamp: bigint,
|
|
670
907
|
signalType: GovernanceSignalAction,
|
|
671
908
|
payload: EthAddress,
|
|
672
909
|
base: IEmpireBase,
|
|
@@ -744,11 +981,30 @@ export class SequencerPublisher {
|
|
|
744
981
|
lastValidL2Slot: slotNumber,
|
|
745
982
|
});
|
|
746
983
|
|
|
984
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
985
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
986
|
+
|
|
747
987
|
try {
|
|
748
988
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
749
989
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
750
990
|
} catch (err) {
|
|
751
|
-
|
|
991
|
+
const viemError = formatViemError(err);
|
|
992
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
993
|
+
simulationTimestamp: timestamp,
|
|
994
|
+
l1BlockNumber,
|
|
995
|
+
});
|
|
996
|
+
this.backupFailedTx({
|
|
997
|
+
id: keccak256(request.data!),
|
|
998
|
+
failureType: 'simulation',
|
|
999
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1000
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1001
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1002
|
+
context: {
|
|
1003
|
+
actions: [action],
|
|
1004
|
+
slot: slotNumber,
|
|
1005
|
+
sender: this.getSenderAddress().toString(),
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
752
1008
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
753
1009
|
}
|
|
754
1010
|
|
|
@@ -799,19 +1055,16 @@ export class SequencerPublisher {
|
|
|
799
1055
|
/**
|
|
800
1056
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
801
1057
|
* @param slotNumber - The slot number to cast a signal for.
|
|
802
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
803
1058
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
804
1059
|
*/
|
|
805
1060
|
public enqueueGovernanceCastSignal(
|
|
806
1061
|
governancePayload: EthAddress,
|
|
807
1062
|
slotNumber: SlotNumber,
|
|
808
|
-
timestamp: bigint,
|
|
809
1063
|
signerAddress: EthAddress,
|
|
810
1064
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
811
1065
|
): Promise<boolean> {
|
|
812
1066
|
return this.enqueueCastSignalHelper(
|
|
813
1067
|
slotNumber,
|
|
814
|
-
timestamp,
|
|
815
1068
|
'governance-signal',
|
|
816
1069
|
governancePayload,
|
|
817
1070
|
this.govProposerContract,
|
|
@@ -824,7 +1077,6 @@ export class SequencerPublisher {
|
|
|
824
1077
|
public async enqueueSlashingActions(
|
|
825
1078
|
actions: ProposerSlashAction[],
|
|
826
1079
|
slotNumber: SlotNumber,
|
|
827
|
-
timestamp: bigint,
|
|
828
1080
|
signerAddress: EthAddress,
|
|
829
1081
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
830
1082
|
): Promise<boolean> {
|
|
@@ -835,58 +1087,6 @@ export class SequencerPublisher {
|
|
|
835
1087
|
|
|
836
1088
|
for (const action of actions) {
|
|
837
1089
|
switch (action.type) {
|
|
838
|
-
case 'vote-empire-payload': {
|
|
839
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
840
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
844
|
-
signerAddress,
|
|
845
|
-
});
|
|
846
|
-
await this.enqueueCastSignalHelper(
|
|
847
|
-
slotNumber,
|
|
848
|
-
timestamp,
|
|
849
|
-
'empire-slashing-signal',
|
|
850
|
-
action.payload,
|
|
851
|
-
this.slashingProposerContract,
|
|
852
|
-
signerAddress,
|
|
853
|
-
signer,
|
|
854
|
-
);
|
|
855
|
-
break;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
case 'create-empire-payload': {
|
|
859
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
860
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
861
|
-
await this.simulateAndEnqueueRequest(
|
|
862
|
-
'create-empire-payload',
|
|
863
|
-
request,
|
|
864
|
-
(receipt: TransactionReceipt) =>
|
|
865
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
866
|
-
slotNumber,
|
|
867
|
-
timestamp,
|
|
868
|
-
);
|
|
869
|
-
break;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
case 'execute-empire-payload': {
|
|
873
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
874
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
875
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
876
|
-
return false;
|
|
877
|
-
}
|
|
878
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
879
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
880
|
-
await this.simulateAndEnqueueRequest(
|
|
881
|
-
'execute-empire-payload',
|
|
882
|
-
request,
|
|
883
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
884
|
-
slotNumber,
|
|
885
|
-
timestamp,
|
|
886
|
-
);
|
|
887
|
-
break;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
1090
|
case 'vote-offenses': {
|
|
891
1091
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
892
1092
|
slotNumber,
|
|
@@ -894,19 +1094,17 @@ export class SequencerPublisher {
|
|
|
894
1094
|
votesCount: action.votes.length,
|
|
895
1095
|
signerAddress,
|
|
896
1096
|
});
|
|
897
|
-
if (this.slashingProposerContract
|
|
898
|
-
this.log.error('
|
|
1097
|
+
if (!this.slashingProposerContract) {
|
|
1098
|
+
this.log.error('No slashing proposer contract available');
|
|
899
1099
|
return false;
|
|
900
1100
|
}
|
|
901
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
902
1101
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
903
|
-
const request = await
|
|
1102
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
904
1103
|
await this.simulateAndEnqueueRequest(
|
|
905
1104
|
'vote-offenses',
|
|
906
1105
|
request,
|
|
907
|
-
(receipt: TransactionReceipt) => !!
|
|
1106
|
+
(receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
|
|
908
1107
|
slotNumber,
|
|
909
|
-
timestamp,
|
|
910
1108
|
);
|
|
911
1109
|
break;
|
|
912
1110
|
}
|
|
@@ -917,18 +1115,20 @@ export class SequencerPublisher {
|
|
|
917
1115
|
round: action.round,
|
|
918
1116
|
signerAddress,
|
|
919
1117
|
});
|
|
920
|
-
if (this.slashingProposerContract
|
|
921
|
-
this.log.error('
|
|
1118
|
+
if (!this.slashingProposerContract) {
|
|
1119
|
+
this.log.error('No slashing proposer contract available');
|
|
922
1120
|
return false;
|
|
923
1121
|
}
|
|
924
|
-
const
|
|
925
|
-
|
|
1122
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1123
|
+
action.round,
|
|
1124
|
+
action.committees,
|
|
1125
|
+
);
|
|
926
1126
|
await this.simulateAndEnqueueRequest(
|
|
927
1127
|
'execute-slash',
|
|
928
|
-
|
|
929
|
-
(receipt: TransactionReceipt) =>
|
|
1128
|
+
executeRequest,
|
|
1129
|
+
(receipt: TransactionReceipt) =>
|
|
1130
|
+
!!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
|
|
930
1131
|
slotNumber,
|
|
931
|
-
timestamp,
|
|
932
1132
|
);
|
|
933
1133
|
break;
|
|
934
1134
|
}
|
|
@@ -948,7 +1148,7 @@ export class SequencerPublisher {
|
|
|
948
1148
|
checkpoint: Checkpoint,
|
|
949
1149
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
950
1150
|
attestationsAndSignersSignature: Signature,
|
|
951
|
-
opts:
|
|
1151
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
952
1152
|
): Promise<void> {
|
|
953
1153
|
const checkpointHeader = checkpoint.header;
|
|
954
1154
|
|
|
@@ -964,7 +1164,9 @@ export class SequencerPublisher {
|
|
|
964
1164
|
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
965
1165
|
};
|
|
966
1166
|
|
|
967
|
-
|
|
1167
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
|
|
1168
|
+
.withoutBlobCheck()
|
|
1169
|
+
.build();
|
|
968
1170
|
|
|
969
1171
|
try {
|
|
970
1172
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
@@ -972,23 +1174,47 @@ export class SequencerPublisher {
|
|
|
972
1174
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
973
1175
|
// make time consistency checks break.
|
|
974
1176
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
975
|
-
|
|
1177
|
+
await this.validateCheckpointForSubmission(
|
|
976
1178
|
checkpoint,
|
|
977
1179
|
attestationsAndSigners,
|
|
978
1180
|
attestationsAndSignersSignature,
|
|
979
|
-
|
|
1181
|
+
simulationOverridesPlan,
|
|
980
1182
|
);
|
|
981
1183
|
} catch (err: any) {
|
|
982
1184
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
983
1185
|
...checkpoint.getStats(),
|
|
984
1186
|
slotNumber: checkpoint.header.slotNumber,
|
|
985
|
-
|
|
1187
|
+
simulationOverridesPlan,
|
|
986
1188
|
});
|
|
987
1189
|
throw err;
|
|
988
1190
|
}
|
|
989
1191
|
|
|
990
|
-
|
|
991
|
-
|
|
1192
|
+
// Build a pre-check callback that re-validates the checkpoint before L1 submission.
|
|
1193
|
+
// During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
|
|
1194
|
+
let preCheck = undefined;
|
|
1195
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
1196
|
+
preCheck = async () => {
|
|
1197
|
+
this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
|
|
1198
|
+
await this.validateCheckpointForSubmission(
|
|
1199
|
+
checkpoint,
|
|
1200
|
+
attestationsAndSigners,
|
|
1201
|
+
attestationsAndSignersSignature,
|
|
1202
|
+
simulationOverridesPlan,
|
|
1203
|
+
);
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1208
|
+
...checkpoint.toCheckpointInfo(),
|
|
1209
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1210
|
+
simulationOverridesPlan,
|
|
1211
|
+
});
|
|
1212
|
+
await this.addProposeTx(
|
|
1213
|
+
checkpoint,
|
|
1214
|
+
proposeTxArgs,
|
|
1215
|
+
{ txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
|
|
1216
|
+
preCheck,
|
|
1217
|
+
);
|
|
992
1218
|
}
|
|
993
1219
|
|
|
994
1220
|
public enqueueInvalidateCheckpoint(
|
|
@@ -1031,8 +1257,8 @@ export class SequencerPublisher {
|
|
|
1031
1257
|
request: L1TxRequest,
|
|
1032
1258
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
1033
1259
|
slotNumber: SlotNumber,
|
|
1034
|
-
timestamp: bigint,
|
|
1035
1260
|
) {
|
|
1261
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1036
1262
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
1037
1263
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
1038
1264
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
@@ -1044,15 +1270,31 @@ export class SequencerPublisher {
|
|
|
1044
1270
|
|
|
1045
1271
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1046
1272
|
|
|
1273
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1274
|
+
|
|
1047
1275
|
let gasUsed: bigint;
|
|
1048
1276
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1277
|
+
|
|
1049
1278
|
try {
|
|
1050
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1279
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1051
1280
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1052
1281
|
} catch (err) {
|
|
1053
1282
|
const viemError = formatViemError(err, simulateAbi);
|
|
1054
1283
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1055
1284
|
|
|
1285
|
+
this.backupFailedTx({
|
|
1286
|
+
id: keccak256(request.data!),
|
|
1287
|
+
failureType: 'simulation',
|
|
1288
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1289
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1290
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1291
|
+
context: {
|
|
1292
|
+
actions: [action],
|
|
1293
|
+
slot: slotNumber,
|
|
1294
|
+
sender: this.getSenderAddress().toString(),
|
|
1295
|
+
},
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1056
1298
|
return false;
|
|
1057
1299
|
}
|
|
1058
1300
|
|
|
@@ -1092,6 +1334,7 @@ export class SequencerPublisher {
|
|
|
1092
1334
|
*/
|
|
1093
1335
|
public interrupt() {
|
|
1094
1336
|
this.interrupted = true;
|
|
1337
|
+
this.interruptibleSleep.interrupt();
|
|
1095
1338
|
this.l1TxUtils.interrupt();
|
|
1096
1339
|
}
|
|
1097
1340
|
|
|
@@ -1101,11 +1344,7 @@ export class SequencerPublisher {
|
|
|
1101
1344
|
this.l1TxUtils.restart();
|
|
1102
1345
|
}
|
|
1103
1346
|
|
|
1104
|
-
private async prepareProposeTx(
|
|
1105
|
-
encodedData: L1ProcessArgs,
|
|
1106
|
-
timestamp: bigint,
|
|
1107
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1108
|
-
) {
|
|
1347
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
1109
1348
|
const kzg = Blob.getViemKzgInstance();
|
|
1110
1349
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1111
1350
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1136,9 +1375,27 @@ export class SequencerPublisher {
|
|
|
1136
1375
|
kzg,
|
|
1137
1376
|
},
|
|
1138
1377
|
)
|
|
1139
|
-
.catch(err => {
|
|
1140
|
-
const
|
|
1141
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1378
|
+
.catch(async err => {
|
|
1379
|
+
const viemError = formatViemError(err);
|
|
1380
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1381
|
+
const validateBlobsData = encodeFunctionData({
|
|
1382
|
+
abi: RollupAbi,
|
|
1383
|
+
functionName: 'validateBlobs',
|
|
1384
|
+
args: [blobInput],
|
|
1385
|
+
});
|
|
1386
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1387
|
+
this.backupFailedTx({
|
|
1388
|
+
id: keccak256(validateBlobsData),
|
|
1389
|
+
failureType: 'simulation',
|
|
1390
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1391
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1392
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1393
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1394
|
+
context: {
|
|
1395
|
+
actions: ['validate-blobs'],
|
|
1396
|
+
sender: this.getSenderAddress().toString(),
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1142
1399
|
throw new Error('Failed to validate blobs');
|
|
1143
1400
|
});
|
|
1144
1401
|
}
|
|
@@ -1158,7 +1415,7 @@ export class SequencerPublisher {
|
|
|
1158
1415
|
blobInput,
|
|
1159
1416
|
] as const;
|
|
1160
1417
|
|
|
1161
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1418
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1162
1419
|
|
|
1163
1420
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1164
1421
|
}
|
|
@@ -1166,7 +1423,6 @@ export class SequencerPublisher {
|
|
|
1166
1423
|
/**
|
|
1167
1424
|
* Simulates the propose tx with eth_simulateV1
|
|
1168
1425
|
* @param args - The propose tx args
|
|
1169
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1170
1426
|
* @returns The simulation result
|
|
1171
1427
|
*/
|
|
1172
1428
|
private async simulateProposeTx(
|
|
@@ -1183,8 +1439,7 @@ export class SequencerPublisher {
|
|
|
1183
1439
|
ViemSignature,
|
|
1184
1440
|
`0x${string}`,
|
|
1185
1441
|
],
|
|
1186
|
-
|
|
1187
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1442
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
1188
1443
|
) {
|
|
1189
1444
|
const rollupData = encodeFunctionData({
|
|
1190
1445
|
abi: RollupAbi,
|
|
@@ -1192,23 +1447,7 @@ export class SequencerPublisher {
|
|
|
1192
1447
|
args,
|
|
1193
1448
|
});
|
|
1194
1449
|
|
|
1195
|
-
|
|
1196
|
-
const forcePendingCheckpointNumberStateDiff = (
|
|
1197
|
-
options.forcePendingCheckpointNumber !== undefined
|
|
1198
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1199
|
-
: []
|
|
1200
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
1201
|
-
|
|
1202
|
-
const stateOverrides: StateOverride = [
|
|
1203
|
-
{
|
|
1204
|
-
address: this.rollupContract.address,
|
|
1205
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1206
|
-
stateDiff: [
|
|
1207
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1208
|
-
...forcePendingCheckpointNumberStateDiff,
|
|
1209
|
-
],
|
|
1210
|
-
},
|
|
1211
|
-
];
|
|
1450
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1212
1451
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1213
1452
|
if (this.proposerAddressForSimulation) {
|
|
1214
1453
|
stateOverrides.push({
|
|
@@ -1217,6 +1456,9 @@ export class SequencerPublisher {
|
|
|
1217
1456
|
});
|
|
1218
1457
|
}
|
|
1219
1458
|
|
|
1459
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1460
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1461
|
+
|
|
1220
1462
|
const simulationResult = await this.l1TxUtils
|
|
1221
1463
|
.simulate(
|
|
1222
1464
|
{
|
|
@@ -1226,8 +1468,7 @@ export class SequencerPublisher {
|
|
|
1226
1468
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1227
1469
|
},
|
|
1228
1470
|
{
|
|
1229
|
-
|
|
1230
|
-
time: timestamp + 1n,
|
|
1471
|
+
time: simTs,
|
|
1231
1472
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1232
1473
|
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1233
1474
|
},
|
|
@@ -1249,7 +1490,19 @@ export class SequencerPublisher {
|
|
|
1249
1490
|
logs: [],
|
|
1250
1491
|
};
|
|
1251
1492
|
}
|
|
1252
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1493
|
+
this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
|
|
1494
|
+
this.backupFailedTx({
|
|
1495
|
+
id: keccak256(rollupData),
|
|
1496
|
+
failureType: 'simulation',
|
|
1497
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1498
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1499
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1500
|
+
context: {
|
|
1501
|
+
actions: ['propose'],
|
|
1502
|
+
slot: Number(args[0].header.slotNumber),
|
|
1503
|
+
sender: this.getSenderAddress().toString(),
|
|
1504
|
+
},
|
|
1505
|
+
});
|
|
1253
1506
|
throw err;
|
|
1254
1507
|
});
|
|
1255
1508
|
|
|
@@ -1259,16 +1512,15 @@ export class SequencerPublisher {
|
|
|
1259
1512
|
private async addProposeTx(
|
|
1260
1513
|
checkpoint: Checkpoint,
|
|
1261
1514
|
encodedData: L1ProcessArgs,
|
|
1262
|
-
opts:
|
|
1263
|
-
|
|
1515
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1516
|
+
preCheck?: () => Promise<void>,
|
|
1264
1517
|
): Promise<void> {
|
|
1265
1518
|
const slot = checkpoint.header.slotNumber;
|
|
1266
1519
|
const timer = new Timer();
|
|
1267
1520
|
const kzg = Blob.getViemKzgInstance();
|
|
1268
1521
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1269
1522
|
encodedData,
|
|
1270
|
-
|
|
1271
|
-
opts,
|
|
1523
|
+
opts.simulationOverridesPlan,
|
|
1272
1524
|
);
|
|
1273
1525
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1274
1526
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
@@ -1292,7 +1544,8 @@ export class SequencerPublisher {
|
|
|
1292
1544
|
data: rollupData,
|
|
1293
1545
|
},
|
|
1294
1546
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1295
|
-
gasConfig: {
|
|
1547
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
|
|
1548
|
+
preCheck,
|
|
1296
1549
|
blobConfig: {
|
|
1297
1550
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1298
1551
|
kzg,
|
|
@@ -1345,4 +1598,17 @@ export class SequencerPublisher {
|
|
|
1345
1598
|
},
|
|
1346
1599
|
});
|
|
1347
1600
|
}
|
|
1601
|
+
|
|
1602
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1603
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
|
|
1604
|
+
private getSimulationTimestamp(slot: SlotNumber): bigint {
|
|
1605
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1606
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1610
|
+
private getNextL1SlotTimestamp(): bigint {
|
|
1611
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1612
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1613
|
+
}
|
|
1348
1614
|
}
|