@aztec/sequencer-client 0.0.1-commit.e558bd1c → 0.0.1-commit.e588bc7e5
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 +15 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +60 -26
- package/dest/config.d.ts +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +47 -28
- package/dest/global_variable_builder/global_builder.d.ts +15 -11
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +29 -25
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- 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 +59 -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 +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- package/dest/publisher/sequencer-publisher.d.ts +76 -30
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +396 -71
- package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -8
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +345 -172
- 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 +21 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +97 -15
- package/dest/sequencer/sequencer.d.ts +40 -17
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +145 -91
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +76 -23
- package/src/config.ts +65 -38
- package/src/global_variable_builder/global_builder.ts +38 -27
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +153 -43
- 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 +55 -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 +38 -6
- package/src/publisher/sequencer-publisher.ts +442 -95
- package/src/sequencer/checkpoint_proposal_job.ts +454 -178
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +106 -18
- package/src/sequencer/sequencer.ts +208 -107
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +63 -49
- package/src/test/utils.ts +5 -2
|
@@ -4,6 +4,8 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
+
FeeAssetPriceOracle,
|
|
8
|
+
type FeeHeader,
|
|
7
9
|
type GovernanceProposerContract,
|
|
8
10
|
type IEmpireBase,
|
|
9
11
|
MULTI_CALL_3_ADDRESS,
|
|
@@ -18,36 +20,65 @@ import {
|
|
|
18
20
|
type L1BlobInputs,
|
|
19
21
|
type L1TxConfig,
|
|
20
22
|
type L1TxRequest,
|
|
23
|
+
type L1TxUtils,
|
|
21
24
|
MAX_L1_TX_LIMIT,
|
|
22
25
|
type TransactionStats,
|
|
23
26
|
WEI_CONST,
|
|
24
27
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
28
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
29
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
28
30
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
29
31
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
32
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
30
33
|
import { pick } from '@aztec/foundation/collection';
|
|
31
34
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
32
36
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
37
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
34
38
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
39
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
35
41
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
42
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
43
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
38
44
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
39
45
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
40
46
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
47
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
41
48
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
42
49
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
50
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
51
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
45
52
|
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
import {
|
|
54
|
+
type Hex,
|
|
55
|
+
type StateOverride,
|
|
56
|
+
type TransactionReceipt,
|
|
57
|
+
type TypedDataDefinition,
|
|
58
|
+
encodeFunctionData,
|
|
59
|
+
keccak256,
|
|
60
|
+
multicall3Abi,
|
|
61
|
+
toHex,
|
|
62
|
+
} from 'viem';
|
|
63
|
+
|
|
64
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
65
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
49
66
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
67
|
|
|
68
|
+
/** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
|
|
69
|
+
export type SendRequestsResult = {
|
|
70
|
+
/** The L1 transaction receipt or error from the bundled multicall. */
|
|
71
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
|
|
72
|
+
/** Actions that expired (past their deadline) before the request was sent. */
|
|
73
|
+
expiredActions: Action[];
|
|
74
|
+
/** Actions that were included in the sent L1 transaction. */
|
|
75
|
+
sentActions: Action[];
|
|
76
|
+
/** Actions whose L1 simulation succeeded (subset of sentActions). */
|
|
77
|
+
successfulActions: Action[];
|
|
78
|
+
/** Actions whose L1 simulation failed (subset of sentActions). */
|
|
79
|
+
failedActions: Action[];
|
|
80
|
+
};
|
|
81
|
+
|
|
51
82
|
/** Arguments to the process method of the rollup contract */
|
|
52
83
|
type L1ProcessArgs = {
|
|
53
84
|
/** The L2 block header. */
|
|
@@ -60,6 +91,8 @@ type L1ProcessArgs = {
|
|
|
60
91
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
92
|
/** Attestations and signers signature */
|
|
62
93
|
attestationsAndSignersSignature: Signature;
|
|
94
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
95
|
+
feeAssetPriceModifier: bigint;
|
|
63
96
|
};
|
|
64
97
|
|
|
65
98
|
export const Actions = [
|
|
@@ -87,6 +120,8 @@ export type InvalidateCheckpointRequest = {
|
|
|
87
120
|
gasUsed: bigint;
|
|
88
121
|
checkpointNumber: CheckpointNumber;
|
|
89
122
|
forcePendingCheckpointNumber: CheckpointNumber;
|
|
123
|
+
/** Archive at the rollback target checkpoint (checkpoint N-1). */
|
|
124
|
+
lastArchive: Fr;
|
|
90
125
|
};
|
|
91
126
|
|
|
92
127
|
interface RequestWithExpiry {
|
|
@@ -105,6 +140,7 @@ export class SequencerPublisher {
|
|
|
105
140
|
private interrupted = false;
|
|
106
141
|
private metrics: SequencerPublisherMetrics;
|
|
107
142
|
public epochCache: EpochCache;
|
|
143
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
108
144
|
|
|
109
145
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
110
146
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -112,24 +148,39 @@ export class SequencerPublisher {
|
|
|
112
148
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
149
|
|
|
114
150
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
151
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
115
152
|
|
|
116
153
|
protected log: Logger;
|
|
117
154
|
protected ethereumSlotDuration: bigint;
|
|
155
|
+
protected aztecSlotDuration: bigint;
|
|
156
|
+
|
|
157
|
+
/** Date provider for wall-clock time. */
|
|
158
|
+
private readonly dateProvider: DateProvider;
|
|
118
159
|
|
|
119
160
|
private blobClient: BlobClientInterface;
|
|
120
161
|
|
|
121
162
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
122
163
|
private proposerAddressForSimulation?: EthAddress;
|
|
123
164
|
|
|
165
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
166
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
167
|
+
|
|
124
168
|
/** L1 fee analyzer for fisherman mode */
|
|
125
169
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
170
|
+
|
|
171
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
172
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
173
|
+
|
|
174
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
175
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
176
|
+
|
|
126
177
|
// A CALL to a cold address is 2700 gas
|
|
127
178
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
179
|
|
|
129
180
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
130
181
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
131
182
|
|
|
132
|
-
public l1TxUtils:
|
|
183
|
+
public l1TxUtils: L1TxUtils;
|
|
133
184
|
public rollupContract: RollupContract;
|
|
134
185
|
public govProposerContract: GovernanceProposerContract;
|
|
135
186
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -140,11 +191,12 @@ export class SequencerPublisher {
|
|
|
140
191
|
protected requests: RequestWithExpiry[] = [];
|
|
141
192
|
|
|
142
193
|
constructor(
|
|
143
|
-
private config:
|
|
194
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
195
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
144
196
|
deps: {
|
|
145
197
|
telemetry?: TelemetryClient;
|
|
146
198
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
199
|
+
l1TxUtils: L1TxUtils;
|
|
148
200
|
rollupContract: RollupContract;
|
|
149
201
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
150
202
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -154,19 +206,24 @@ export class SequencerPublisher {
|
|
|
154
206
|
metrics: SequencerPublisherMetrics;
|
|
155
207
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
156
208
|
log?: Logger;
|
|
209
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
157
210
|
},
|
|
158
211
|
) {
|
|
159
212
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
160
213
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
214
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
215
|
+
this.dateProvider = deps.dateProvider;
|
|
161
216
|
this.epochCache = deps.epochCache;
|
|
162
217
|
this.lastActions = deps.lastActions;
|
|
163
218
|
|
|
164
219
|
this.blobClient = deps.blobClient;
|
|
220
|
+
this.dateProvider = deps.dateProvider;
|
|
165
221
|
|
|
166
222
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
167
223
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
168
224
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
169
225
|
this.l1TxUtils = deps.l1TxUtils;
|
|
226
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
170
227
|
|
|
171
228
|
this.rollupContract = deps.rollupContract;
|
|
172
229
|
|
|
@@ -188,12 +245,52 @@ export class SequencerPublisher {
|
|
|
188
245
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
246
|
);
|
|
190
247
|
}
|
|
248
|
+
|
|
249
|
+
// Initialize fee asset price oracle
|
|
250
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
251
|
+
this.l1TxUtils.client,
|
|
252
|
+
this.rollupContract,
|
|
253
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
257
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
262
|
+
* Does nothing if no store is configured.
|
|
263
|
+
*/
|
|
264
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
265
|
+
if (!this.failedTxStore) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const tx: FailedL1Tx = {
|
|
270
|
+
...failedTx,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Fire and forget - don't block on backup
|
|
275
|
+
void this.failedTxStore
|
|
276
|
+
.then(store => store?.saveFailedTx(tx))
|
|
277
|
+
.catch(err => {
|
|
278
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
279
|
+
});
|
|
191
280
|
}
|
|
192
281
|
|
|
193
282
|
public getRollupContract(): RollupContract {
|
|
194
283
|
return this.rollupContract;
|
|
195
284
|
}
|
|
196
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Gets the fee asset price modifier from the oracle.
|
|
288
|
+
* Returns 0n if the oracle query fails.
|
|
289
|
+
*/
|
|
290
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
291
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
292
|
+
}
|
|
293
|
+
|
|
197
294
|
public getSenderAddress() {
|
|
198
295
|
return this.l1TxUtils.getSenderAddress();
|
|
199
296
|
}
|
|
@@ -218,7 +315,7 @@ export class SequencerPublisher {
|
|
|
218
315
|
}
|
|
219
316
|
|
|
220
317
|
public getCurrentL2Slot(): SlotNumber {
|
|
221
|
-
return this.epochCache.
|
|
318
|
+
return this.epochCache.getSlotNow();
|
|
222
319
|
}
|
|
223
320
|
|
|
224
321
|
/**
|
|
@@ -296,9 +393,10 @@ export class SequencerPublisher {
|
|
|
296
393
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
297
394
|
*/
|
|
298
395
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
299
|
-
public async sendRequests() {
|
|
396
|
+
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
300
397
|
const requestsToProcess = [...this.requests];
|
|
301
398
|
this.requests = [];
|
|
399
|
+
|
|
302
400
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
303
401
|
return undefined;
|
|
304
402
|
}
|
|
@@ -331,8 +429,8 @@ export class SequencerPublisher {
|
|
|
331
429
|
// @note - we can only have one blob config per bundle
|
|
332
430
|
// find requests with gas and blob configs
|
|
333
431
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
334
|
-
const gasConfigs =
|
|
335
|
-
const blobConfigs =
|
|
432
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
433
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
336
434
|
|
|
337
435
|
if (blobConfigs.length > 1) {
|
|
338
436
|
throw new Error('Multiple blob configs found');
|
|
@@ -361,19 +459,36 @@ export class SequencerPublisher {
|
|
|
361
459
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
362
460
|
|
|
363
461
|
try {
|
|
462
|
+
// Capture context for failed tx backup before sending
|
|
463
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
464
|
+
const multicallData = encodeFunctionData({
|
|
465
|
+
abi: multicall3Abi,
|
|
466
|
+
functionName: 'aggregate3',
|
|
467
|
+
args: [
|
|
468
|
+
validRequests.map(r => ({
|
|
469
|
+
target: r.request.to!,
|
|
470
|
+
callData: r.request.data!,
|
|
471
|
+
allowFailure: true,
|
|
472
|
+
})),
|
|
473
|
+
],
|
|
474
|
+
});
|
|
475
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
476
|
+
|
|
477
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
478
|
+
|
|
364
479
|
this.log.debug('Forwarding transactions', {
|
|
365
480
|
validRequests: validRequests.map(request => request.action),
|
|
366
481
|
txConfig,
|
|
367
482
|
});
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
483
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
484
|
+
if (result === undefined) {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
488
|
+
validRequests,
|
|
489
|
+
result,
|
|
490
|
+
txContext,
|
|
375
491
|
);
|
|
376
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
377
492
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
378
493
|
} catch (err) {
|
|
379
494
|
const viemError = formatViemError(err);
|
|
@@ -391,16 +506,105 @@ export class SequencerPublisher {
|
|
|
391
506
|
}
|
|
392
507
|
}
|
|
393
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
511
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
512
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
513
|
+
*/
|
|
514
|
+
private async forwardWithPublisherRotation(
|
|
515
|
+
validRequests: RequestWithExpiry[],
|
|
516
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
517
|
+
blobConfig: L1BlobInputs | undefined,
|
|
518
|
+
) {
|
|
519
|
+
const triedAddresses: EthAddress[] = [];
|
|
520
|
+
let currentPublisher = this.l1TxUtils;
|
|
521
|
+
|
|
522
|
+
while (true) {
|
|
523
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
524
|
+
try {
|
|
525
|
+
const result = await Multicall3.forward(
|
|
526
|
+
validRequests.map(r => r.request),
|
|
527
|
+
currentPublisher,
|
|
528
|
+
txConfig,
|
|
529
|
+
blobConfig,
|
|
530
|
+
this.rollupContract.address,
|
|
531
|
+
this.log,
|
|
532
|
+
);
|
|
533
|
+
this.l1TxUtils = currentPublisher;
|
|
534
|
+
return result;
|
|
535
|
+
} catch (err) {
|
|
536
|
+
if (err instanceof TimeoutError) {
|
|
537
|
+
throw err;
|
|
538
|
+
}
|
|
539
|
+
const viemError = formatViemError(err);
|
|
540
|
+
if (!this.getNextPublisher) {
|
|
541
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
this.log.warn(
|
|
545
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
546
|
+
viemError,
|
|
547
|
+
);
|
|
548
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
549
|
+
if (!nextPublisher) {
|
|
550
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
currentPublisher = nextPublisher;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/*
|
|
559
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
560
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
561
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
562
|
+
*/
|
|
563
|
+
public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
|
|
564
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
565
|
+
if (ms > 0) {
|
|
566
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
|
|
567
|
+
await this.interruptibleSleep.sleep(ms);
|
|
568
|
+
}
|
|
569
|
+
if (this.interrupted) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
return this.sendRequests();
|
|
573
|
+
}
|
|
574
|
+
|
|
394
575
|
private callbackBundledTransactions(
|
|
395
576
|
requests: RequestWithExpiry[],
|
|
396
|
-
result
|
|
577
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
578
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
397
579
|
) {
|
|
398
580
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
399
581
|
if (result instanceof FormattedViemError) {
|
|
400
582
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
583
|
+
this.backupFailedTx({
|
|
584
|
+
id: keccak256(txContext.multicallData),
|
|
585
|
+
failureType: 'send-error',
|
|
586
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
587
|
+
blobData: txContext.blobData,
|
|
588
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
589
|
+
error: { message: result.message, name: result.name },
|
|
590
|
+
context: {
|
|
591
|
+
actions: requests.map(r => r.action),
|
|
592
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
593
|
+
sender: this.getSenderAddress().toString(),
|
|
594
|
+
},
|
|
595
|
+
});
|
|
401
596
|
return { failedActions: requests.map(r => r.action) };
|
|
402
597
|
} else {
|
|
403
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
598
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
599
|
+
result,
|
|
600
|
+
requests: requests.map(r => ({
|
|
601
|
+
...r,
|
|
602
|
+
// Avoid logging large blob data
|
|
603
|
+
blobConfig: r.blobConfig
|
|
604
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
605
|
+
: undefined,
|
|
606
|
+
})),
|
|
607
|
+
});
|
|
404
608
|
const successfulActions: Action[] = [];
|
|
405
609
|
const failedActions: Action[] = [];
|
|
406
610
|
for (const request of requests) {
|
|
@@ -410,26 +614,59 @@ export class SequencerPublisher {
|
|
|
410
614
|
failedActions.push(request.action);
|
|
411
615
|
}
|
|
412
616
|
}
|
|
617
|
+
// Single backup for the whole reverted tx
|
|
618
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
619
|
+
this.backupFailedTx({
|
|
620
|
+
id: result.receipt.transactionHash,
|
|
621
|
+
failureType: 'revert',
|
|
622
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
623
|
+
blobData: txContext.blobData,
|
|
624
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
625
|
+
receipt: {
|
|
626
|
+
transactionHash: result.receipt.transactionHash,
|
|
627
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
628
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
629
|
+
status: 'reverted',
|
|
630
|
+
},
|
|
631
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
632
|
+
context: {
|
|
633
|
+
actions: failedActions,
|
|
634
|
+
requests: requests
|
|
635
|
+
.filter(r => failedActions.includes(r.action))
|
|
636
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
637
|
+
sender: this.getSenderAddress().toString(),
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
}
|
|
413
641
|
return { successfulActions, failedActions };
|
|
414
642
|
}
|
|
415
643
|
}
|
|
416
644
|
|
|
417
645
|
/**
|
|
418
|
-
* @notice Will call `
|
|
646
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
419
647
|
* @param tipArchive - The archive to check
|
|
420
648
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
421
649
|
*/
|
|
422
|
-
public
|
|
650
|
+
public canProposeAt(
|
|
423
651
|
tipArchive: Fr,
|
|
424
652
|
msgSender: EthAddress,
|
|
425
|
-
opts: {
|
|
653
|
+
opts: {
|
|
654
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
655
|
+
forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
|
|
656
|
+
pipelined?: boolean;
|
|
657
|
+
} = {},
|
|
426
658
|
) {
|
|
427
659
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
428
660
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
429
661
|
|
|
662
|
+
const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
|
|
663
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
664
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
665
|
+
|
|
430
666
|
return this.rollupContract
|
|
431
|
-
.
|
|
667
|
+
.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
432
668
|
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
669
|
+
forceArchive: opts.forceArchive,
|
|
433
670
|
})
|
|
434
671
|
.catch(err => {
|
|
435
672
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
@@ -442,6 +679,7 @@ export class SequencerPublisher {
|
|
|
442
679
|
return undefined;
|
|
443
680
|
});
|
|
444
681
|
}
|
|
682
|
+
|
|
445
683
|
/**
|
|
446
684
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
447
685
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
@@ -465,7 +703,7 @@ export class SequencerPublisher {
|
|
|
465
703
|
flags,
|
|
466
704
|
] as const;
|
|
467
705
|
|
|
468
|
-
const ts =
|
|
706
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
469
707
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
470
708
|
opts?.forcePendingCheckpointNumber,
|
|
471
709
|
);
|
|
@@ -488,7 +726,7 @@ export class SequencerPublisher {
|
|
|
488
726
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
489
727
|
from: MULTI_CALL_3_ADDRESS,
|
|
490
728
|
},
|
|
491
|
-
{ time: ts
|
|
729
|
+
{ time: ts },
|
|
492
730
|
stateOverrides,
|
|
493
731
|
);
|
|
494
732
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -521,6 +759,8 @@ export class SequencerPublisher {
|
|
|
521
759
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
760
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
523
761
|
|
|
762
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
763
|
+
|
|
524
764
|
try {
|
|
525
765
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
766
|
request,
|
|
@@ -539,6 +779,7 @@ export class SequencerPublisher {
|
|
|
539
779
|
gasUsed,
|
|
540
780
|
checkpointNumber,
|
|
541
781
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
782
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
542
783
|
reason,
|
|
543
784
|
};
|
|
544
785
|
} catch (err) {
|
|
@@ -551,8 +792,8 @@ export class SequencerPublisher {
|
|
|
551
792
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
552
793
|
{ ...logData, request, error: viemError.message },
|
|
553
794
|
);
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
795
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
796
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
556
797
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
557
798
|
return undefined;
|
|
558
799
|
} else {
|
|
@@ -572,6 +813,18 @@ export class SequencerPublisher {
|
|
|
572
813
|
|
|
573
814
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
815
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
816
|
+
this.backupFailedTx({
|
|
817
|
+
id: keccak256(request.data!),
|
|
818
|
+
failureType: 'simulation',
|
|
819
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
820
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
821
|
+
error: { message: viemError.message, name: viemError.name },
|
|
822
|
+
context: {
|
|
823
|
+
actions: [`invalidate-${reason}`],
|
|
824
|
+
checkpointNumber,
|
|
825
|
+
sender: this.getSenderAddress().toString(),
|
|
826
|
+
},
|
|
827
|
+
});
|
|
575
828
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
576
829
|
}
|
|
577
830
|
}
|
|
@@ -614,27 +867,13 @@ export class SequencerPublisher {
|
|
|
614
867
|
checkpoint: Checkpoint,
|
|
615
868
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
616
869
|
attestationsAndSignersSignature: Signature,
|
|
617
|
-
options: {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
623
|
-
// so that the committee is recalculated correctly
|
|
624
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
625
|
-
// if (ignoreSignatures) {
|
|
626
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
627
|
-
// if (!committee) {
|
|
628
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
629
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
630
|
-
// }
|
|
631
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
632
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
633
|
-
// );
|
|
634
|
-
// }
|
|
635
|
-
|
|
870
|
+
options: {
|
|
871
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
872
|
+
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
873
|
+
},
|
|
874
|
+
): Promise<void> {
|
|
636
875
|
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
876
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
638
877
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
878
|
|
|
640
879
|
const args = [
|
|
@@ -642,7 +881,7 @@ export class SequencerPublisher {
|
|
|
642
881
|
header: checkpoint.header.toViem(),
|
|
643
882
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
883
|
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier:
|
|
884
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
646
885
|
},
|
|
647
886
|
},
|
|
648
887
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -651,13 +890,11 @@ export class SequencerPublisher {
|
|
|
651
890
|
blobInput,
|
|
652
891
|
] as const;
|
|
653
892
|
|
|
654
|
-
await this.simulateProposeTx(args,
|
|
655
|
-
return ts;
|
|
893
|
+
await this.simulateProposeTx(args, options);
|
|
656
894
|
}
|
|
657
895
|
|
|
658
896
|
private async enqueueCastSignalHelper(
|
|
659
897
|
slotNumber: SlotNumber,
|
|
660
|
-
timestamp: bigint,
|
|
661
898
|
signalType: GovernanceSignalAction,
|
|
662
899
|
payload: EthAddress,
|
|
663
900
|
base: IEmpireBase,
|
|
@@ -691,6 +928,32 @@ export class SequencerPublisher {
|
|
|
691
928
|
return false;
|
|
692
929
|
}
|
|
693
930
|
|
|
931
|
+
// Check if payload was already submitted to governance
|
|
932
|
+
const cacheKey = payload.toString();
|
|
933
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
934
|
+
try {
|
|
935
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
936
|
+
const proposed = await retry(
|
|
937
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
938
|
+
'Check if payload was proposed',
|
|
939
|
+
makeBackoff([0, 1, 2]),
|
|
940
|
+
this.log,
|
|
941
|
+
true,
|
|
942
|
+
);
|
|
943
|
+
if (proposed) {
|
|
944
|
+
this.payloadProposedCache.add(cacheKey);
|
|
945
|
+
}
|
|
946
|
+
} catch (err) {
|
|
947
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
953
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
|
|
694
957
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
958
|
this.lastActions[signalType] = slotNumber;
|
|
696
959
|
const action = signalType;
|
|
@@ -709,11 +972,30 @@ export class SequencerPublisher {
|
|
|
709
972
|
lastValidL2Slot: slotNumber,
|
|
710
973
|
});
|
|
711
974
|
|
|
975
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
976
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
977
|
+
|
|
712
978
|
try {
|
|
713
979
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
714
980
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
715
981
|
} catch (err) {
|
|
716
|
-
|
|
982
|
+
const viemError = formatViemError(err);
|
|
983
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
984
|
+
simulationTimestamp: timestamp,
|
|
985
|
+
l1BlockNumber,
|
|
986
|
+
});
|
|
987
|
+
this.backupFailedTx({
|
|
988
|
+
id: keccak256(request.data!),
|
|
989
|
+
failureType: 'simulation',
|
|
990
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
991
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
992
|
+
error: { message: viemError.message, name: viemError.name },
|
|
993
|
+
context: {
|
|
994
|
+
actions: [action],
|
|
995
|
+
slot: slotNumber,
|
|
996
|
+
sender: this.getSenderAddress().toString(),
|
|
997
|
+
},
|
|
998
|
+
});
|
|
717
999
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
718
1000
|
}
|
|
719
1001
|
|
|
@@ -764,19 +1046,16 @@ export class SequencerPublisher {
|
|
|
764
1046
|
/**
|
|
765
1047
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
766
1048
|
* @param slotNumber - The slot number to cast a signal for.
|
|
767
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
768
1049
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
769
1050
|
*/
|
|
770
1051
|
public enqueueGovernanceCastSignal(
|
|
771
1052
|
governancePayload: EthAddress,
|
|
772
1053
|
slotNumber: SlotNumber,
|
|
773
|
-
timestamp: bigint,
|
|
774
1054
|
signerAddress: EthAddress,
|
|
775
1055
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
776
1056
|
): Promise<boolean> {
|
|
777
1057
|
return this.enqueueCastSignalHelper(
|
|
778
1058
|
slotNumber,
|
|
779
|
-
timestamp,
|
|
780
1059
|
'governance-signal',
|
|
781
1060
|
governancePayload,
|
|
782
1061
|
this.govProposerContract,
|
|
@@ -789,7 +1068,6 @@ export class SequencerPublisher {
|
|
|
789
1068
|
public async enqueueSlashingActions(
|
|
790
1069
|
actions: ProposerSlashAction[],
|
|
791
1070
|
slotNumber: SlotNumber,
|
|
792
|
-
timestamp: bigint,
|
|
793
1071
|
signerAddress: EthAddress,
|
|
794
1072
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
795
1073
|
): Promise<boolean> {
|
|
@@ -810,7 +1088,6 @@ export class SequencerPublisher {
|
|
|
810
1088
|
});
|
|
811
1089
|
await this.enqueueCastSignalHelper(
|
|
812
1090
|
slotNumber,
|
|
813
|
-
timestamp,
|
|
814
1091
|
'empire-slashing-signal',
|
|
815
1092
|
action.payload,
|
|
816
1093
|
this.slashingProposerContract,
|
|
@@ -829,7 +1106,6 @@ export class SequencerPublisher {
|
|
|
829
1106
|
(receipt: TransactionReceipt) =>
|
|
830
1107
|
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
831
1108
|
slotNumber,
|
|
832
|
-
timestamp,
|
|
833
1109
|
);
|
|
834
1110
|
break;
|
|
835
1111
|
}
|
|
@@ -847,7 +1123,6 @@ export class SequencerPublisher {
|
|
|
847
1123
|
request,
|
|
848
1124
|
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
849
1125
|
slotNumber,
|
|
850
|
-
timestamp,
|
|
851
1126
|
);
|
|
852
1127
|
break;
|
|
853
1128
|
}
|
|
@@ -871,7 +1146,6 @@ export class SequencerPublisher {
|
|
|
871
1146
|
request,
|
|
872
1147
|
(receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
|
|
873
1148
|
slotNumber,
|
|
874
|
-
timestamp,
|
|
875
1149
|
);
|
|
876
1150
|
break;
|
|
877
1151
|
}
|
|
@@ -893,7 +1167,6 @@ export class SequencerPublisher {
|
|
|
893
1167
|
request,
|
|
894
1168
|
(receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
|
|
895
1169
|
slotNumber,
|
|
896
|
-
timestamp,
|
|
897
1170
|
);
|
|
898
1171
|
break;
|
|
899
1172
|
}
|
|
@@ -913,30 +1186,33 @@ export class SequencerPublisher {
|
|
|
913
1186
|
checkpoint: Checkpoint,
|
|
914
1187
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
915
1188
|
attestationsAndSignersSignature: Signature,
|
|
916
|
-
opts: {
|
|
1189
|
+
opts: {
|
|
1190
|
+
txTimeoutAt?: Date;
|
|
1191
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1192
|
+
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1193
|
+
} = {},
|
|
917
1194
|
): Promise<void> {
|
|
918
1195
|
const checkpointHeader = checkpoint.header;
|
|
919
1196
|
|
|
920
1197
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1198
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
1199
|
|
|
923
|
-
const proposeTxArgs = {
|
|
1200
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
1201
|
header: checkpointHeader,
|
|
925
1202
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
1203
|
blobs,
|
|
927
1204
|
attestationsAndSigners,
|
|
928
1205
|
attestationsAndSignersSignature,
|
|
1206
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
1207
|
};
|
|
930
1208
|
|
|
931
|
-
let ts: bigint;
|
|
932
|
-
|
|
933
1209
|
try {
|
|
934
1210
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
935
1211
|
// This means that we can avoid the simulation issues in later checks.
|
|
936
1212
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
937
1213
|
// make time consistency checks break.
|
|
938
1214
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
939
|
-
|
|
1215
|
+
await this.validateCheckpointForSubmission(
|
|
940
1216
|
checkpoint,
|
|
941
1217
|
attestationsAndSigners,
|
|
942
1218
|
attestationsAndSignersSignature,
|
|
@@ -952,7 +1228,7 @@ export class SequencerPublisher {
|
|
|
952
1228
|
}
|
|
953
1229
|
|
|
954
1230
|
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
955
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts
|
|
1231
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts);
|
|
956
1232
|
}
|
|
957
1233
|
|
|
958
1234
|
public enqueueInvalidateCheckpoint(
|
|
@@ -995,8 +1271,8 @@ export class SequencerPublisher {
|
|
|
995
1271
|
request: L1TxRequest,
|
|
996
1272
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
997
1273
|
slotNumber: SlotNumber,
|
|
998
|
-
timestamp: bigint,
|
|
999
1274
|
) {
|
|
1275
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1000
1276
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
1001
1277
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
1002
1278
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
@@ -1008,15 +1284,31 @@ export class SequencerPublisher {
|
|
|
1008
1284
|
|
|
1009
1285
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1010
1286
|
|
|
1287
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1288
|
+
|
|
1011
1289
|
let gasUsed: bigint;
|
|
1012
1290
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1291
|
+
|
|
1013
1292
|
try {
|
|
1014
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1293
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1015
1294
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1016
1295
|
} catch (err) {
|
|
1017
1296
|
const viemError = formatViemError(err, simulateAbi);
|
|
1018
1297
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
1298
|
|
|
1299
|
+
this.backupFailedTx({
|
|
1300
|
+
id: keccak256(request.data!),
|
|
1301
|
+
failureType: 'simulation',
|
|
1302
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1303
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1304
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1305
|
+
context: {
|
|
1306
|
+
actions: [action],
|
|
1307
|
+
slot: slotNumber,
|
|
1308
|
+
sender: this.getSenderAddress().toString(),
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1020
1312
|
return false;
|
|
1021
1313
|
}
|
|
1022
1314
|
|
|
@@ -1056,6 +1348,7 @@ export class SequencerPublisher {
|
|
|
1056
1348
|
*/
|
|
1057
1349
|
public interrupt() {
|
|
1058
1350
|
this.interrupted = true;
|
|
1351
|
+
this.interruptibleSleep.interrupt();
|
|
1059
1352
|
this.l1TxUtils.interrupt();
|
|
1060
1353
|
}
|
|
1061
1354
|
|
|
@@ -1067,7 +1360,6 @@ export class SequencerPublisher {
|
|
|
1067
1360
|
|
|
1068
1361
|
private async prepareProposeTx(
|
|
1069
1362
|
encodedData: L1ProcessArgs,
|
|
1070
|
-
timestamp: bigint,
|
|
1071
1363
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1072
1364
|
) {
|
|
1073
1365
|
const kzg = Blob.getViemKzgInstance();
|
|
@@ -1100,9 +1392,27 @@ export class SequencerPublisher {
|
|
|
1100
1392
|
kzg,
|
|
1101
1393
|
},
|
|
1102
1394
|
)
|
|
1103
|
-
.catch(err => {
|
|
1104
|
-
const
|
|
1105
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1395
|
+
.catch(async err => {
|
|
1396
|
+
const viemError = formatViemError(err);
|
|
1397
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1398
|
+
const validateBlobsData = encodeFunctionData({
|
|
1399
|
+
abi: RollupAbi,
|
|
1400
|
+
functionName: 'validateBlobs',
|
|
1401
|
+
args: [blobInput],
|
|
1402
|
+
});
|
|
1403
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1404
|
+
this.backupFailedTx({
|
|
1405
|
+
id: keccak256(validateBlobsData),
|
|
1406
|
+
failureType: 'simulation',
|
|
1407
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1408
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1409
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1410
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1411
|
+
context: {
|
|
1412
|
+
actions: ['validate-blobs'],
|
|
1413
|
+
sender: this.getSenderAddress().toString(),
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1106
1416
|
throw new Error('Failed to validate blobs');
|
|
1107
1417
|
});
|
|
1108
1418
|
}
|
|
@@ -1113,8 +1423,7 @@ export class SequencerPublisher {
|
|
|
1113
1423
|
header: encodedData.header.toViem(),
|
|
1114
1424
|
archive: toHex(encodedData.archive),
|
|
1115
1425
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1426
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1427
|
},
|
|
1119
1428
|
},
|
|
1120
1429
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1123,7 +1432,7 @@ export class SequencerPublisher {
|
|
|
1123
1432
|
blobInput,
|
|
1124
1433
|
] as const;
|
|
1125
1434
|
|
|
1126
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1435
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
|
|
1127
1436
|
|
|
1128
1437
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1129
1438
|
}
|
|
@@ -1131,7 +1440,6 @@ export class SequencerPublisher {
|
|
|
1131
1440
|
/**
|
|
1132
1441
|
* Simulates the propose tx with eth_simulateV1
|
|
1133
1442
|
* @param args - The propose tx args
|
|
1134
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1135
1443
|
* @returns The simulation result
|
|
1136
1444
|
*/
|
|
1137
1445
|
private async simulateProposeTx(
|
|
@@ -1140,7 +1448,7 @@ export class SequencerPublisher {
|
|
|
1140
1448
|
readonly header: ViemHeader;
|
|
1141
1449
|
readonly archive: `0x${string}`;
|
|
1142
1450
|
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier:
|
|
1451
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1144
1452
|
};
|
|
1145
1453
|
},
|
|
1146
1454
|
ViemCommitteeAttestations,
|
|
@@ -1148,8 +1456,10 @@ export class SequencerPublisher {
|
|
|
1148
1456
|
ViemSignature,
|
|
1149
1457
|
`0x${string}`,
|
|
1150
1458
|
],
|
|
1151
|
-
|
|
1152
|
-
|
|
1459
|
+
options: {
|
|
1460
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1461
|
+
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1462
|
+
},
|
|
1153
1463
|
) {
|
|
1154
1464
|
const rollupData = encodeFunctionData({
|
|
1155
1465
|
abi: RollupAbi,
|
|
@@ -1157,13 +1467,23 @@ export class SequencerPublisher {
|
|
|
1157
1467
|
args,
|
|
1158
1468
|
});
|
|
1159
1469
|
|
|
1160
|
-
// override the
|
|
1470
|
+
// override the proposed checkpoint number if requested
|
|
1161
1471
|
const forcePendingCheckpointNumberStateDiff = (
|
|
1162
1472
|
options.forcePendingCheckpointNumber !== undefined
|
|
1163
1473
|
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1164
1474
|
: []
|
|
1165
1475
|
).flatMap(override => override.stateDiff ?? []);
|
|
1166
1476
|
|
|
1477
|
+
// override the fee header for a specific checkpoint number if requested (used when pipelining)
|
|
1478
|
+
const forceProposedFeeHeaderStateDiff = (
|
|
1479
|
+
options.forceProposedFeeHeader !== undefined
|
|
1480
|
+
? await this.rollupContract.makeFeeHeaderOverride(
|
|
1481
|
+
options.forceProposedFeeHeader.checkpointNumber,
|
|
1482
|
+
options.forceProposedFeeHeader.feeHeader,
|
|
1483
|
+
)
|
|
1484
|
+
: []
|
|
1485
|
+
).flatMap(override => override.stateDiff ?? []);
|
|
1486
|
+
|
|
1167
1487
|
const stateOverrides: StateOverride = [
|
|
1168
1488
|
{
|
|
1169
1489
|
address: this.rollupContract.address,
|
|
@@ -1171,6 +1491,7 @@ export class SequencerPublisher {
|
|
|
1171
1491
|
stateDiff: [
|
|
1172
1492
|
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1173
1493
|
...forcePendingCheckpointNumberStateDiff,
|
|
1494
|
+
...forceProposedFeeHeaderStateDiff,
|
|
1174
1495
|
],
|
|
1175
1496
|
},
|
|
1176
1497
|
];
|
|
@@ -1182,6 +1503,9 @@ export class SequencerPublisher {
|
|
|
1182
1503
|
});
|
|
1183
1504
|
}
|
|
1184
1505
|
|
|
1506
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1507
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1508
|
+
|
|
1185
1509
|
const simulationResult = await this.l1TxUtils
|
|
1186
1510
|
.simulate(
|
|
1187
1511
|
{
|
|
@@ -1191,8 +1515,7 @@ export class SequencerPublisher {
|
|
|
1191
1515
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1192
1516
|
},
|
|
1193
1517
|
{
|
|
1194
|
-
|
|
1195
|
-
time: timestamp + 1n,
|
|
1518
|
+
time: simTs,
|
|
1196
1519
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1197
1520
|
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1198
1521
|
},
|
|
@@ -1214,7 +1537,19 @@ export class SequencerPublisher {
|
|
|
1214
1537
|
logs: [],
|
|
1215
1538
|
};
|
|
1216
1539
|
}
|
|
1217
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1540
|
+
this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
|
|
1541
|
+
this.backupFailedTx({
|
|
1542
|
+
id: keccak256(rollupData),
|
|
1543
|
+
failureType: 'simulation',
|
|
1544
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1545
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1546
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1547
|
+
context: {
|
|
1548
|
+
actions: ['propose'],
|
|
1549
|
+
slot: Number(args[0].header.slotNumber),
|
|
1550
|
+
sender: this.getSenderAddress().toString(),
|
|
1551
|
+
},
|
|
1552
|
+
});
|
|
1218
1553
|
throw err;
|
|
1219
1554
|
});
|
|
1220
1555
|
|
|
@@ -1224,17 +1559,16 @@ export class SequencerPublisher {
|
|
|
1224
1559
|
private async addProposeTx(
|
|
1225
1560
|
checkpoint: Checkpoint,
|
|
1226
1561
|
encodedData: L1ProcessArgs,
|
|
1227
|
-
opts: {
|
|
1228
|
-
|
|
1562
|
+
opts: {
|
|
1563
|
+
txTimeoutAt?: Date;
|
|
1564
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1565
|
+
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1566
|
+
} = {},
|
|
1229
1567
|
): Promise<void> {
|
|
1230
1568
|
const slot = checkpoint.header.slotNumber;
|
|
1231
1569
|
const timer = new Timer();
|
|
1232
1570
|
const kzg = Blob.getViemKzgInstance();
|
|
1233
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1234
|
-
encodedData,
|
|
1235
|
-
timestamp,
|
|
1236
|
-
opts,
|
|
1237
|
-
);
|
|
1571
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
|
|
1238
1572
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1239
1573
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
1240
1574
|
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
@@ -1310,4 +1644,17 @@ export class SequencerPublisher {
|
|
|
1310
1644
|
},
|
|
1311
1645
|
});
|
|
1312
1646
|
}
|
|
1647
|
+
|
|
1648
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1649
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
|
|
1650
|
+
private getSimulationTimestamp(slot: SlotNumber): bigint {
|
|
1651
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1652
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1656
|
+
private getNextL1SlotTimestamp(): bigint {
|
|
1657
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1658
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1659
|
+
}
|
|
1313
1660
|
}
|