@aztec/sequencer-client 0.0.1-commit.5914bae → 0.0.1-commit.59a0419c6
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 +2 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +8 -8
- package/dest/config.d.ts +3 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +10 -12
- 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 +3 -8
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +4 -39
- package/dest/global_variable_builder/index.d.ts +3 -1
- 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 +52 -41
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +345 -101
- 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 +38 -8
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +427 -171
- 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/events.d.ts +7 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +11 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +38 -20
- package/dest/sequencer/sequencer.d.ts +17 -5
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +120 -58
- package/dest/sequencer/timetable.d.ts +14 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +45 -36
- package/dest/test/mock_checkpoint_builder.d.ts +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +0 -2
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +8 -14
- package/src/config.ts +12 -11
- 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 +8 -43
- package/src/global_variable_builder/index.ts +2 -0
- 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 +389 -140
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +517 -180
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/events.ts +6 -1
- package/src/sequencer/metrics.ts +49 -25
- package/src/sequencer/sequencer.ts +177 -63
- package/src/sequencer/timetable.ts +57 -45
- package/src/test/mock_checkpoint_builder.ts +0 -2
|
@@ -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,32 +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';
|
|
31
32
|
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
32
33
|
import { pick } from '@aztec/foundation/collection';
|
|
33
34
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
34
36
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
35
37
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
36
38
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
37
39
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
38
41
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
39
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
42
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
40
43
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
41
44
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
42
45
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
43
46
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
44
47
|
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
45
|
-
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
46
48
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
47
49
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
48
50
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
49
51
|
|
|
50
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
type Hex,
|
|
54
|
+
type TransactionReceipt,
|
|
55
|
+
type TypedDataDefinition,
|
|
56
|
+
encodeFunctionData,
|
|
57
|
+
keccak256,
|
|
58
|
+
multicall3Abi,
|
|
59
|
+
toHex,
|
|
60
|
+
} from 'viem';
|
|
51
61
|
|
|
52
62
|
import type { SequencerPublisherConfig } from './config.js';
|
|
63
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
53
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
54
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
|
+
|
|
55
80
|
/** Arguments to the process method of the rollup contract */
|
|
56
81
|
type L1ProcessArgs = {
|
|
57
82
|
/** The L2 block header. */
|
|
@@ -73,16 +98,13 @@ export const Actions = [
|
|
|
73
98
|
'invalidate-by-insufficient-attestations',
|
|
74
99
|
'propose',
|
|
75
100
|
'governance-signal',
|
|
76
|
-
'empire-slashing-signal',
|
|
77
|
-
'create-empire-payload',
|
|
78
|
-
'execute-empire-payload',
|
|
79
101
|
'vote-offenses',
|
|
80
102
|
'execute-slash',
|
|
81
103
|
] as const;
|
|
82
104
|
|
|
83
105
|
export type Action = (typeof Actions)[number];
|
|
84
106
|
|
|
85
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
107
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
86
108
|
|
|
87
109
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
88
110
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -93,6 +115,13 @@ export type InvalidateCheckpointRequest = {
|
|
|
93
115
|
gasUsed: bigint;
|
|
94
116
|
checkpointNumber: CheckpointNumber;
|
|
95
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;
|
|
96
125
|
};
|
|
97
126
|
|
|
98
127
|
interface RequestWithExpiry {
|
|
@@ -101,6 +130,8 @@ interface RequestWithExpiry {
|
|
|
101
130
|
lastValidL2Slot: SlotNumber;
|
|
102
131
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
103
132
|
blobConfig?: L1BlobInputs;
|
|
133
|
+
/** Optional pre-send validation. If it rejects, the request is discarded. */
|
|
134
|
+
preCheck?: () => Promise<void>;
|
|
104
135
|
checkSuccess: (
|
|
105
136
|
request: L1TxRequest,
|
|
106
137
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -111,6 +142,7 @@ export class SequencerPublisher {
|
|
|
111
142
|
private interrupted = false;
|
|
112
143
|
private metrics: SequencerPublisherMetrics;
|
|
113
144
|
public epochCache: EpochCache;
|
|
145
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
114
146
|
|
|
115
147
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
116
148
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -123,19 +155,27 @@ export class SequencerPublisher {
|
|
|
123
155
|
protected log: Logger;
|
|
124
156
|
protected ethereumSlotDuration: bigint;
|
|
125
157
|
protected aztecSlotDuration: bigint;
|
|
126
|
-
|
|
158
|
+
|
|
159
|
+
/** Date provider for wall-clock time. */
|
|
160
|
+
private readonly dateProvider: DateProvider;
|
|
127
161
|
|
|
128
162
|
private blobClient: BlobClientInterface;
|
|
129
163
|
|
|
130
164
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
131
165
|
private proposerAddressForSimulation?: EthAddress;
|
|
132
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
|
+
|
|
133
170
|
/** L1 fee analyzer for fisherman mode */
|
|
134
171
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
135
172
|
|
|
136
173
|
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
137
174
|
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
138
175
|
|
|
176
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
177
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
178
|
+
|
|
139
179
|
// A CALL to a cold address is 2700 gas
|
|
140
180
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
141
181
|
|
|
@@ -145,29 +185,28 @@ export class SequencerPublisher {
|
|
|
145
185
|
public l1TxUtils: L1TxUtils;
|
|
146
186
|
public rollupContract: RollupContract;
|
|
147
187
|
public govProposerContract: GovernanceProposerContract;
|
|
148
|
-
public slashingProposerContract:
|
|
149
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
188
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
150
189
|
|
|
151
190
|
public readonly tracer: Tracer;
|
|
152
191
|
|
|
153
192
|
protected requests: RequestWithExpiry[] = [];
|
|
154
193
|
|
|
155
194
|
constructor(
|
|
156
|
-
private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
|
|
195
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
157
196
|
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
158
197
|
deps: {
|
|
159
198
|
telemetry?: TelemetryClient;
|
|
160
199
|
blobClient: BlobClientInterface;
|
|
161
200
|
l1TxUtils: L1TxUtils;
|
|
162
201
|
rollupContract: RollupContract;
|
|
163
|
-
slashingProposerContract:
|
|
202
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
164
203
|
governanceProposerContract: GovernanceProposerContract;
|
|
165
|
-
slashFactoryContract: SlashFactoryContract;
|
|
166
204
|
epochCache: EpochCache;
|
|
167
205
|
dateProvider: DateProvider;
|
|
168
206
|
metrics: SequencerPublisherMetrics;
|
|
169
207
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
170
208
|
log?: Logger;
|
|
209
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
171
210
|
},
|
|
172
211
|
) {
|
|
173
212
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
@@ -178,11 +217,13 @@ export class SequencerPublisher {
|
|
|
178
217
|
this.lastActions = deps.lastActions;
|
|
179
218
|
|
|
180
219
|
this.blobClient = deps.blobClient;
|
|
220
|
+
this.dateProvider = deps.dateProvider;
|
|
181
221
|
|
|
182
222
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
183
223
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
184
224
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
185
225
|
this.l1TxUtils = deps.l1TxUtils;
|
|
226
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
186
227
|
|
|
187
228
|
this.rollupContract = deps.rollupContract;
|
|
188
229
|
|
|
@@ -194,8 +235,6 @@ export class SequencerPublisher {
|
|
|
194
235
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
195
236
|
this.slashingProposerContract = newSlashingProposer;
|
|
196
237
|
});
|
|
197
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
198
|
-
|
|
199
238
|
// Initialize L1 fee analyzer for fisherman mode
|
|
200
239
|
if (config.fishermanMode) {
|
|
201
240
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
@@ -211,6 +250,31 @@ export class SequencerPublisher {
|
|
|
211
250
|
this.rollupContract,
|
|
212
251
|
createLogger('sequencer:publisher:price-oracle'),
|
|
213
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
|
+
});
|
|
214
278
|
}
|
|
215
279
|
|
|
216
280
|
public getRollupContract(): RollupContract {
|
|
@@ -249,7 +313,7 @@ export class SequencerPublisher {
|
|
|
249
313
|
}
|
|
250
314
|
|
|
251
315
|
public getCurrentL2Slot(): SlotNumber {
|
|
252
|
-
return this.epochCache.
|
|
316
|
+
return this.epochCache.getSlotNow();
|
|
253
317
|
}
|
|
254
318
|
|
|
255
319
|
/**
|
|
@@ -327,9 +391,10 @@ export class SequencerPublisher {
|
|
|
327
391
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
328
392
|
*/
|
|
329
393
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
330
|
-
public async sendRequests() {
|
|
394
|
+
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
331
395
|
const requestsToProcess = [...this.requests];
|
|
332
396
|
this.requests = [];
|
|
397
|
+
|
|
333
398
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
334
399
|
return undefined;
|
|
335
400
|
}
|
|
@@ -392,19 +457,36 @@ export class SequencerPublisher {
|
|
|
392
457
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
393
458
|
|
|
394
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
|
+
|
|
395
477
|
this.log.debug('Forwarding transactions', {
|
|
396
478
|
validRequests: validRequests.map(request => request.action),
|
|
397
479
|
txConfig,
|
|
398
480
|
});
|
|
399
|
-
const result = await
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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,
|
|
406
489
|
);
|
|
407
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
408
490
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
409
491
|
} catch (err) {
|
|
410
492
|
const viemError = formatViemError(err);
|
|
@@ -422,13 +504,115 @@ export class SequencerPublisher {
|
|
|
422
504
|
}
|
|
423
505
|
}
|
|
424
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
|
+
|
|
425
595
|
private callbackBundledTransactions(
|
|
426
596
|
requests: RequestWithExpiry[],
|
|
427
|
-
result
|
|
597
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
598
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
428
599
|
) {
|
|
429
600
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
430
601
|
if (result instanceof FormattedViemError) {
|
|
431
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
|
+
});
|
|
432
616
|
return { failedActions: requests.map(r => r.action) };
|
|
433
617
|
} else {
|
|
434
618
|
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
@@ -450,6 +634,30 @@ export class SequencerPublisher {
|
|
|
450
634
|
failedActions.push(request.action);
|
|
451
635
|
}
|
|
452
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
|
+
}
|
|
453
661
|
return { successfulActions, failedActions };
|
|
454
662
|
}
|
|
455
663
|
}
|
|
@@ -459,22 +667,21 @@ export class SequencerPublisher {
|
|
|
459
667
|
* @param tipArchive - The archive to check
|
|
460
668
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
461
669
|
*/
|
|
462
|
-
public canProposeAt(
|
|
463
|
-
tipArchive: Fr,
|
|
464
|
-
msgSender: EthAddress,
|
|
465
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
|
|
466
|
-
) {
|
|
670
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
467
671
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
468
672
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
469
673
|
|
|
470
|
-
const pipelined =
|
|
674
|
+
const pipelined = this.epochCache.isProposerPipeliningEnabled();
|
|
471
675
|
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
472
676
|
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
473
677
|
|
|
474
678
|
return this.rollupContract
|
|
475
|
-
.canProposeAt(
|
|
476
|
-
|
|
477
|
-
|
|
679
|
+
.canProposeAt(
|
|
680
|
+
tipArchive.toBuffer(),
|
|
681
|
+
msgSender.toString(),
|
|
682
|
+
nextL1SlotTs,
|
|
683
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
684
|
+
)
|
|
478
685
|
.catch(err => {
|
|
479
686
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
480
687
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -486,6 +693,7 @@ export class SequencerPublisher {
|
|
|
486
693
|
return undefined;
|
|
487
694
|
});
|
|
488
695
|
}
|
|
696
|
+
|
|
489
697
|
/**
|
|
490
698
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
491
699
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
@@ -495,7 +703,7 @@ export class SequencerPublisher {
|
|
|
495
703
|
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
496
704
|
public async validateBlockHeader(
|
|
497
705
|
header: CheckpointHeader,
|
|
498
|
-
|
|
706
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
499
707
|
): Promise<void> {
|
|
500
708
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
501
709
|
|
|
@@ -510,9 +718,7 @@ export class SequencerPublisher {
|
|
|
510
718
|
] as const;
|
|
511
719
|
|
|
512
720
|
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
513
|
-
const stateOverrides = await this.rollupContract
|
|
514
|
-
opts?.forcePendingCheckpointNumber,
|
|
515
|
-
);
|
|
721
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
516
722
|
let balance = 0n;
|
|
517
723
|
if (this.config.fishermanMode) {
|
|
518
724
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -565,6 +771,8 @@ export class SequencerPublisher {
|
|
|
565
771
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
566
772
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
567
773
|
|
|
774
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
775
|
+
|
|
568
776
|
try {
|
|
569
777
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
570
778
|
request,
|
|
@@ -583,6 +791,7 @@ export class SequencerPublisher {
|
|
|
583
791
|
gasUsed,
|
|
584
792
|
checkpointNumber,
|
|
585
793
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
794
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
586
795
|
reason,
|
|
587
796
|
};
|
|
588
797
|
} catch (err) {
|
|
@@ -595,8 +804,8 @@ export class SequencerPublisher {
|
|
|
595
804
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
596
805
|
{ ...logData, request, error: viemError.message },
|
|
597
806
|
);
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
807
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
808
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
600
809
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
601
810
|
return undefined;
|
|
602
811
|
} else {
|
|
@@ -616,6 +825,18 @@ export class SequencerPublisher {
|
|
|
616
825
|
|
|
617
826
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
618
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
|
+
});
|
|
619
840
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
620
841
|
}
|
|
621
842
|
}
|
|
@@ -658,7 +879,7 @@ export class SequencerPublisher {
|
|
|
658
879
|
checkpoint: Checkpoint,
|
|
659
880
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
660
881
|
attestationsAndSignersSignature: Signature,
|
|
661
|
-
|
|
882
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
662
883
|
): Promise<void> {
|
|
663
884
|
const blobFields = checkpoint.toBlobFields();
|
|
664
885
|
const blobs = await getBlobsPerL1Block(blobFields);
|
|
@@ -678,7 +899,7 @@ export class SequencerPublisher {
|
|
|
678
899
|
blobInput,
|
|
679
900
|
] as const;
|
|
680
901
|
|
|
681
|
-
await this.simulateProposeTx(args,
|
|
902
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
682
903
|
}
|
|
683
904
|
|
|
684
905
|
private async enqueueCastSignalHelper(
|
|
@@ -760,6 +981,7 @@ export class SequencerPublisher {
|
|
|
760
981
|
lastValidL2Slot: slotNumber,
|
|
761
982
|
});
|
|
762
983
|
|
|
984
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
763
985
|
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
764
986
|
|
|
765
987
|
try {
|
|
@@ -769,6 +991,19 @@ export class SequencerPublisher {
|
|
|
769
991
|
const viemError = formatViemError(err);
|
|
770
992
|
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
771
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
|
+
},
|
|
772
1007
|
});
|
|
773
1008
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
774
1009
|
}
|
|
@@ -852,55 +1087,6 @@ export class SequencerPublisher {
|
|
|
852
1087
|
|
|
853
1088
|
for (const action of actions) {
|
|
854
1089
|
switch (action.type) {
|
|
855
|
-
case 'vote-empire-payload': {
|
|
856
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
857
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
858
|
-
break;
|
|
859
|
-
}
|
|
860
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
861
|
-
signerAddress,
|
|
862
|
-
});
|
|
863
|
-
await this.enqueueCastSignalHelper(
|
|
864
|
-
slotNumber,
|
|
865
|
-
'empire-slashing-signal',
|
|
866
|
-
action.payload,
|
|
867
|
-
this.slashingProposerContract,
|
|
868
|
-
signerAddress,
|
|
869
|
-
signer,
|
|
870
|
-
);
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
case 'create-empire-payload': {
|
|
875
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
876
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
877
|
-
await this.simulateAndEnqueueRequest(
|
|
878
|
-
'create-empire-payload',
|
|
879
|
-
request,
|
|
880
|
-
(receipt: TransactionReceipt) =>
|
|
881
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
882
|
-
slotNumber,
|
|
883
|
-
);
|
|
884
|
-
break;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
case 'execute-empire-payload': {
|
|
888
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
889
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
890
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
891
|
-
return false;
|
|
892
|
-
}
|
|
893
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
894
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
895
|
-
await this.simulateAndEnqueueRequest(
|
|
896
|
-
'execute-empire-payload',
|
|
897
|
-
request,
|
|
898
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
899
|
-
slotNumber,
|
|
900
|
-
);
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
1090
|
case 'vote-offenses': {
|
|
905
1091
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
906
1092
|
slotNumber,
|
|
@@ -908,17 +1094,16 @@ export class SequencerPublisher {
|
|
|
908
1094
|
votesCount: action.votes.length,
|
|
909
1095
|
signerAddress,
|
|
910
1096
|
});
|
|
911
|
-
if (this.slashingProposerContract
|
|
912
|
-
this.log.error('
|
|
1097
|
+
if (!this.slashingProposerContract) {
|
|
1098
|
+
this.log.error('No slashing proposer contract available');
|
|
913
1099
|
return false;
|
|
914
1100
|
}
|
|
915
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
916
1101
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
917
|
-
const request = await
|
|
1102
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
918
1103
|
await this.simulateAndEnqueueRequest(
|
|
919
1104
|
'vote-offenses',
|
|
920
1105
|
request,
|
|
921
|
-
(receipt: TransactionReceipt) => !!
|
|
1106
|
+
(receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
|
|
922
1107
|
slotNumber,
|
|
923
1108
|
);
|
|
924
1109
|
break;
|
|
@@ -930,16 +1115,19 @@ export class SequencerPublisher {
|
|
|
930
1115
|
round: action.round,
|
|
931
1116
|
signerAddress,
|
|
932
1117
|
});
|
|
933
|
-
if (this.slashingProposerContract
|
|
934
|
-
this.log.error('
|
|
1118
|
+
if (!this.slashingProposerContract) {
|
|
1119
|
+
this.log.error('No slashing proposer contract available');
|
|
935
1120
|
return false;
|
|
936
1121
|
}
|
|
937
|
-
const
|
|
938
|
-
|
|
1122
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1123
|
+
action.round,
|
|
1124
|
+
action.committees,
|
|
1125
|
+
);
|
|
939
1126
|
await this.simulateAndEnqueueRequest(
|
|
940
1127
|
'execute-slash',
|
|
941
|
-
|
|
942
|
-
(receipt: TransactionReceipt) =>
|
|
1128
|
+
executeRequest,
|
|
1129
|
+
(receipt: TransactionReceipt) =>
|
|
1130
|
+
!!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
|
|
943
1131
|
slotNumber,
|
|
944
1132
|
);
|
|
945
1133
|
break;
|
|
@@ -960,7 +1148,7 @@ export class SequencerPublisher {
|
|
|
960
1148
|
checkpoint: Checkpoint,
|
|
961
1149
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
962
1150
|
attestationsAndSignersSignature: Signature,
|
|
963
|
-
opts:
|
|
1151
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
964
1152
|
): Promise<void> {
|
|
965
1153
|
const checkpointHeader = checkpoint.header;
|
|
966
1154
|
|
|
@@ -976,6 +1164,10 @@ export class SequencerPublisher {
|
|
|
976
1164
|
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
977
1165
|
};
|
|
978
1166
|
|
|
1167
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
|
|
1168
|
+
.withoutBlobCheck()
|
|
1169
|
+
.build();
|
|
1170
|
+
|
|
979
1171
|
try {
|
|
980
1172
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
981
1173
|
// This means that we can avoid the simulation issues in later checks.
|
|
@@ -986,19 +1178,43 @@ export class SequencerPublisher {
|
|
|
986
1178
|
checkpoint,
|
|
987
1179
|
attestationsAndSigners,
|
|
988
1180
|
attestationsAndSignersSignature,
|
|
989
|
-
|
|
1181
|
+
simulationOverridesPlan,
|
|
990
1182
|
);
|
|
991
1183
|
} catch (err: any) {
|
|
992
1184
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
993
1185
|
...checkpoint.getStats(),
|
|
994
1186
|
slotNumber: checkpoint.header.slotNumber,
|
|
995
|
-
|
|
1187
|
+
simulationOverridesPlan,
|
|
996
1188
|
});
|
|
997
1189
|
throw err;
|
|
998
1190
|
}
|
|
999
1191
|
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
+
);
|
|
1002
1218
|
}
|
|
1003
1219
|
|
|
1004
1220
|
public enqueueInvalidateCheckpoint(
|
|
@@ -1054,6 +1270,8 @@ export class SequencerPublisher {
|
|
|
1054
1270
|
|
|
1055
1271
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1056
1272
|
|
|
1273
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1274
|
+
|
|
1057
1275
|
let gasUsed: bigint;
|
|
1058
1276
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1059
1277
|
|
|
@@ -1064,6 +1282,19 @@ export class SequencerPublisher {
|
|
|
1064
1282
|
const viemError = formatViemError(err, simulateAbi);
|
|
1065
1283
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1066
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
|
+
|
|
1067
1298
|
return false;
|
|
1068
1299
|
}
|
|
1069
1300
|
|
|
@@ -1103,6 +1334,7 @@ export class SequencerPublisher {
|
|
|
1103
1334
|
*/
|
|
1104
1335
|
public interrupt() {
|
|
1105
1336
|
this.interrupted = true;
|
|
1337
|
+
this.interruptibleSleep.interrupt();
|
|
1106
1338
|
this.l1TxUtils.interrupt();
|
|
1107
1339
|
}
|
|
1108
1340
|
|
|
@@ -1112,10 +1344,7 @@ export class SequencerPublisher {
|
|
|
1112
1344
|
this.l1TxUtils.restart();
|
|
1113
1345
|
}
|
|
1114
1346
|
|
|
1115
|
-
private async prepareProposeTx(
|
|
1116
|
-
encodedData: L1ProcessArgs,
|
|
1117
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1118
|
-
) {
|
|
1347
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
1119
1348
|
const kzg = Blob.getViemKzgInstance();
|
|
1120
1349
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1121
1350
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1146,9 +1375,27 @@ export class SequencerPublisher {
|
|
|
1146
1375
|
kzg,
|
|
1147
1376
|
},
|
|
1148
1377
|
)
|
|
1149
|
-
.catch(err => {
|
|
1150
|
-
const
|
|
1151
|
-
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
|
+
});
|
|
1152
1399
|
throw new Error('Failed to validate blobs');
|
|
1153
1400
|
});
|
|
1154
1401
|
}
|
|
@@ -1168,7 +1415,7 @@ export class SequencerPublisher {
|
|
|
1168
1415
|
blobInput,
|
|
1169
1416
|
] as const;
|
|
1170
1417
|
|
|
1171
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1418
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1172
1419
|
|
|
1173
1420
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1174
1421
|
}
|
|
@@ -1192,7 +1439,7 @@ export class SequencerPublisher {
|
|
|
1192
1439
|
ViemSignature,
|
|
1193
1440
|
`0x${string}`,
|
|
1194
1441
|
],
|
|
1195
|
-
|
|
1442
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
1196
1443
|
) {
|
|
1197
1444
|
const rollupData = encodeFunctionData({
|
|
1198
1445
|
abi: RollupAbi,
|
|
@@ -1200,23 +1447,7 @@ export class SequencerPublisher {
|
|
|
1200
1447
|
args,
|
|
1201
1448
|
});
|
|
1202
1449
|
|
|
1203
|
-
|
|
1204
|
-
const forcePendingCheckpointNumberStateDiff = (
|
|
1205
|
-
options.forcePendingCheckpointNumber !== undefined
|
|
1206
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1207
|
-
: []
|
|
1208
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
1209
|
-
|
|
1210
|
-
const stateOverrides: StateOverride = [
|
|
1211
|
-
{
|
|
1212
|
-
address: this.rollupContract.address,
|
|
1213
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1214
|
-
stateDiff: [
|
|
1215
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1216
|
-
...forcePendingCheckpointNumberStateDiff,
|
|
1217
|
-
],
|
|
1218
|
-
},
|
|
1219
|
-
];
|
|
1450
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1220
1451
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1221
1452
|
if (this.proposerAddressForSimulation) {
|
|
1222
1453
|
stateOverrides.push({
|
|
@@ -1225,6 +1456,7 @@ export class SequencerPublisher {
|
|
|
1225
1456
|
});
|
|
1226
1457
|
}
|
|
1227
1458
|
|
|
1459
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1228
1460
|
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1229
1461
|
|
|
1230
1462
|
const simulationResult = await this.l1TxUtils
|
|
@@ -1259,6 +1491,18 @@ export class SequencerPublisher {
|
|
|
1259
1491
|
};
|
|
1260
1492
|
}
|
|
1261
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
|
+
});
|
|
1262
1506
|
throw err;
|
|
1263
1507
|
});
|
|
1264
1508
|
|
|
@@ -1268,12 +1512,16 @@ export class SequencerPublisher {
|
|
|
1268
1512
|
private async addProposeTx(
|
|
1269
1513
|
checkpoint: Checkpoint,
|
|
1270
1514
|
encodedData: L1ProcessArgs,
|
|
1271
|
-
opts:
|
|
1515
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1516
|
+
preCheck?: () => Promise<void>,
|
|
1272
1517
|
): Promise<void> {
|
|
1273
1518
|
const slot = checkpoint.header.slotNumber;
|
|
1274
1519
|
const timer = new Timer();
|
|
1275
1520
|
const kzg = Blob.getViemKzgInstance();
|
|
1276
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1521
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1522
|
+
encodedData,
|
|
1523
|
+
opts.simulationOverridesPlan,
|
|
1524
|
+
);
|
|
1277
1525
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1278
1526
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
1279
1527
|
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
@@ -1296,7 +1544,8 @@ export class SequencerPublisher {
|
|
|
1296
1544
|
data: rollupData,
|
|
1297
1545
|
},
|
|
1298
1546
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1299
|
-
gasConfig: {
|
|
1547
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
|
|
1548
|
+
preCheck,
|
|
1300
1549
|
blobConfig: {
|
|
1301
1550
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1302
1551
|
kzg,
|