@aztec/sequencer-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ee6fcc6
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 +14 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +22 -21
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +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 +32 -9
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +343 -39
- package/dest/sequencer/checkpoint_proposal_job.d.ts +15 -7
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +240 -139
- 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 +28 -15
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +93 -84
- 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 +23 -24
- 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 +349 -53
- package/src/sequencer/checkpoint_proposal_job.ts +327 -150
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +106 -18
- package/src/sequencer/sequencer.ts +127 -96
- 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,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
+
FeeAssetPriceOracle,
|
|
7
8
|
type GovernanceProposerContract,
|
|
8
9
|
type IEmpireBase,
|
|
9
10
|
MULTI_CALL_3_ADDRESS,
|
|
@@ -18,34 +19,48 @@ import {
|
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
21
23
|
MAX_L1_TX_LIMIT,
|
|
22
24
|
type TransactionStats,
|
|
23
25
|
WEI_CONST,
|
|
24
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
27
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
28
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
28
29
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
29
30
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
31
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
30
32
|
import { pick } from '@aztec/foundation/collection';
|
|
31
33
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
34
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
32
35
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
36
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
34
37
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
38
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
35
39
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
40
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
41
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
38
42
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
39
43
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
40
44
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
45
|
+
import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
41
46
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
42
47
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
48
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
49
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
45
50
|
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
import {
|
|
52
|
+
type Hex,
|
|
53
|
+
type StateOverride,
|
|
54
|
+
type TransactionReceipt,
|
|
55
|
+
type TypedDataDefinition,
|
|
56
|
+
encodeFunctionData,
|
|
57
|
+
keccak256,
|
|
58
|
+
multicall3Abi,
|
|
59
|
+
toHex,
|
|
60
|
+
} from 'viem';
|
|
61
|
+
|
|
62
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
63
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
49
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
65
|
|
|
51
66
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -60,6 +75,8 @@ type L1ProcessArgs = {
|
|
|
60
75
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
76
|
/** Attestations and signers signature */
|
|
62
77
|
attestationsAndSignersSignature: Signature;
|
|
78
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
79
|
+
feeAssetPriceModifier: bigint;
|
|
63
80
|
};
|
|
64
81
|
|
|
65
82
|
export const Actions = [
|
|
@@ -105,6 +122,7 @@ export class SequencerPublisher {
|
|
|
105
122
|
private interrupted = false;
|
|
106
123
|
private metrics: SequencerPublisherMetrics;
|
|
107
124
|
public epochCache: EpochCache;
|
|
125
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
108
126
|
|
|
109
127
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
110
128
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -112,24 +130,34 @@ export class SequencerPublisher {
|
|
|
112
130
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
131
|
|
|
114
132
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
133
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
115
134
|
|
|
116
135
|
protected log: Logger;
|
|
117
136
|
protected ethereumSlotDuration: bigint;
|
|
137
|
+
protected aztecSlotDuration: bigint;
|
|
138
|
+
private dateProvider: DateProvider;
|
|
118
139
|
|
|
119
140
|
private blobClient: BlobClientInterface;
|
|
120
141
|
|
|
121
142
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
122
143
|
private proposerAddressForSimulation?: EthAddress;
|
|
123
144
|
|
|
145
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
146
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
147
|
+
|
|
124
148
|
/** L1 fee analyzer for fisherman mode */
|
|
125
149
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
150
|
+
|
|
151
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
152
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
153
|
+
|
|
126
154
|
// A CALL to a cold address is 2700 gas
|
|
127
155
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
156
|
|
|
129
157
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
130
158
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
131
159
|
|
|
132
|
-
public l1TxUtils:
|
|
160
|
+
public l1TxUtils: L1TxUtils;
|
|
133
161
|
public rollupContract: RollupContract;
|
|
134
162
|
public govProposerContract: GovernanceProposerContract;
|
|
135
163
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
@@ -140,11 +168,12 @@ export class SequencerPublisher {
|
|
|
140
168
|
protected requests: RequestWithExpiry[] = [];
|
|
141
169
|
|
|
142
170
|
constructor(
|
|
143
|
-
private config:
|
|
171
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
172
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
144
173
|
deps: {
|
|
145
174
|
telemetry?: TelemetryClient;
|
|
146
175
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
176
|
+
l1TxUtils: L1TxUtils;
|
|
148
177
|
rollupContract: RollupContract;
|
|
149
178
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
150
179
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -154,10 +183,13 @@ export class SequencerPublisher {
|
|
|
154
183
|
metrics: SequencerPublisherMetrics;
|
|
155
184
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
156
185
|
log?: Logger;
|
|
186
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
157
187
|
},
|
|
158
188
|
) {
|
|
159
189
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
160
190
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
191
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
192
|
+
this.dateProvider = deps.dateProvider;
|
|
161
193
|
this.epochCache = deps.epochCache;
|
|
162
194
|
this.lastActions = deps.lastActions;
|
|
163
195
|
|
|
@@ -167,6 +199,7 @@ export class SequencerPublisher {
|
|
|
167
199
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
168
200
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
169
201
|
this.l1TxUtils = deps.l1TxUtils;
|
|
202
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
170
203
|
|
|
171
204
|
this.rollupContract = deps.rollupContract;
|
|
172
205
|
|
|
@@ -188,12 +221,52 @@ export class SequencerPublisher {
|
|
|
188
221
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
222
|
);
|
|
190
223
|
}
|
|
224
|
+
|
|
225
|
+
// Initialize fee asset price oracle
|
|
226
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
227
|
+
this.l1TxUtils.client,
|
|
228
|
+
this.rollupContract,
|
|
229
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
233
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
238
|
+
* Does nothing if no store is configured.
|
|
239
|
+
*/
|
|
240
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
241
|
+
if (!this.failedTxStore) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const tx: FailedL1Tx = {
|
|
246
|
+
...failedTx,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Fire and forget - don't block on backup
|
|
251
|
+
void this.failedTxStore
|
|
252
|
+
.then(store => store?.saveFailedTx(tx))
|
|
253
|
+
.catch(err => {
|
|
254
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
255
|
+
});
|
|
191
256
|
}
|
|
192
257
|
|
|
193
258
|
public getRollupContract(): RollupContract {
|
|
194
259
|
return this.rollupContract;
|
|
195
260
|
}
|
|
196
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Gets the fee asset price modifier from the oracle.
|
|
264
|
+
* Returns 0n if the oracle query fails.
|
|
265
|
+
*/
|
|
266
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
267
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
268
|
+
}
|
|
269
|
+
|
|
197
270
|
public getSenderAddress() {
|
|
198
271
|
return this.l1TxUtils.getSenderAddress();
|
|
199
272
|
}
|
|
@@ -218,7 +291,7 @@ export class SequencerPublisher {
|
|
|
218
291
|
}
|
|
219
292
|
|
|
220
293
|
public getCurrentL2Slot(): SlotNumber {
|
|
221
|
-
return this.epochCache.
|
|
294
|
+
return this.epochCache.getSlotNow();
|
|
222
295
|
}
|
|
223
296
|
|
|
224
297
|
/**
|
|
@@ -331,8 +404,8 @@ export class SequencerPublisher {
|
|
|
331
404
|
// @note - we can only have one blob config per bundle
|
|
332
405
|
// find requests with gas and blob configs
|
|
333
406
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
334
|
-
const gasConfigs =
|
|
335
|
-
const blobConfigs =
|
|
407
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
408
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
336
409
|
|
|
337
410
|
if (blobConfigs.length > 1) {
|
|
338
411
|
throw new Error('Multiple blob configs found');
|
|
@@ -361,19 +434,36 @@ export class SequencerPublisher {
|
|
|
361
434
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
362
435
|
|
|
363
436
|
try {
|
|
437
|
+
// Capture context for failed tx backup before sending
|
|
438
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
439
|
+
const multicallData = encodeFunctionData({
|
|
440
|
+
abi: multicall3Abi,
|
|
441
|
+
functionName: 'aggregate3',
|
|
442
|
+
args: [
|
|
443
|
+
validRequests.map(r => ({
|
|
444
|
+
target: r.request.to!,
|
|
445
|
+
callData: r.request.data!,
|
|
446
|
+
allowFailure: true,
|
|
447
|
+
})),
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
451
|
+
|
|
452
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
453
|
+
|
|
364
454
|
this.log.debug('Forwarding transactions', {
|
|
365
455
|
validRequests: validRequests.map(request => request.action),
|
|
366
456
|
txConfig,
|
|
367
457
|
});
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
458
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
459
|
+
if (result === undefined) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
463
|
+
validRequests,
|
|
464
|
+
result,
|
|
465
|
+
txContext,
|
|
375
466
|
);
|
|
376
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
377
467
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
378
468
|
} catch (err) {
|
|
379
469
|
const viemError = formatViemError(err);
|
|
@@ -391,16 +481,88 @@ export class SequencerPublisher {
|
|
|
391
481
|
}
|
|
392
482
|
}
|
|
393
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
486
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
487
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
488
|
+
*/
|
|
489
|
+
private async forwardWithPublisherRotation(
|
|
490
|
+
validRequests: RequestWithExpiry[],
|
|
491
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
492
|
+
blobConfig: L1BlobInputs | undefined,
|
|
493
|
+
) {
|
|
494
|
+
const triedAddresses: EthAddress[] = [];
|
|
495
|
+
let currentPublisher = this.l1TxUtils;
|
|
496
|
+
|
|
497
|
+
while (true) {
|
|
498
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
499
|
+
try {
|
|
500
|
+
const result = await Multicall3.forward(
|
|
501
|
+
validRequests.map(r => r.request),
|
|
502
|
+
currentPublisher,
|
|
503
|
+
txConfig,
|
|
504
|
+
blobConfig,
|
|
505
|
+
this.rollupContract.address,
|
|
506
|
+
this.log,
|
|
507
|
+
);
|
|
508
|
+
this.l1TxUtils = currentPublisher;
|
|
509
|
+
return result;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
if (err instanceof TimeoutError) {
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
const viemError = formatViemError(err);
|
|
515
|
+
if (!this.getNextPublisher) {
|
|
516
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
this.log.warn(
|
|
520
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
521
|
+
viemError,
|
|
522
|
+
);
|
|
523
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
524
|
+
if (!nextPublisher) {
|
|
525
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
currentPublisher = nextPublisher;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
394
533
|
private callbackBundledTransactions(
|
|
395
534
|
requests: RequestWithExpiry[],
|
|
396
|
-
result
|
|
535
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
536
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
397
537
|
) {
|
|
398
538
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
399
539
|
if (result instanceof FormattedViemError) {
|
|
400
540
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
541
|
+
this.backupFailedTx({
|
|
542
|
+
id: keccak256(txContext.multicallData),
|
|
543
|
+
failureType: 'send-error',
|
|
544
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
545
|
+
blobData: txContext.blobData,
|
|
546
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
547
|
+
error: { message: result.message, name: result.name },
|
|
548
|
+
context: {
|
|
549
|
+
actions: requests.map(r => r.action),
|
|
550
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
551
|
+
sender: this.getSenderAddress().toString(),
|
|
552
|
+
},
|
|
553
|
+
});
|
|
401
554
|
return { failedActions: requests.map(r => r.action) };
|
|
402
555
|
} else {
|
|
403
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
556
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
557
|
+
result,
|
|
558
|
+
requests: requests.map(r => ({
|
|
559
|
+
...r,
|
|
560
|
+
// Avoid logging large blob data
|
|
561
|
+
blobConfig: r.blobConfig
|
|
562
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
563
|
+
: undefined,
|
|
564
|
+
})),
|
|
565
|
+
});
|
|
404
566
|
const successfulActions: Action[] = [];
|
|
405
567
|
const failedActions: Action[] = [];
|
|
406
568
|
for (const request of requests) {
|
|
@@ -410,25 +572,53 @@ export class SequencerPublisher {
|
|
|
410
572
|
failedActions.push(request.action);
|
|
411
573
|
}
|
|
412
574
|
}
|
|
575
|
+
// Single backup for the whole reverted tx
|
|
576
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
577
|
+
this.backupFailedTx({
|
|
578
|
+
id: result.receipt.transactionHash,
|
|
579
|
+
failureType: 'revert',
|
|
580
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
581
|
+
blobData: txContext.blobData,
|
|
582
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
583
|
+
receipt: {
|
|
584
|
+
transactionHash: result.receipt.transactionHash,
|
|
585
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
586
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
587
|
+
status: 'reverted',
|
|
588
|
+
},
|
|
589
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
590
|
+
context: {
|
|
591
|
+
actions: failedActions,
|
|
592
|
+
requests: requests
|
|
593
|
+
.filter(r => failedActions.includes(r.action))
|
|
594
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
595
|
+
sender: this.getSenderAddress().toString(),
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
413
599
|
return { successfulActions, failedActions };
|
|
414
600
|
}
|
|
415
601
|
}
|
|
416
602
|
|
|
417
603
|
/**
|
|
418
|
-
* @notice Will call `
|
|
604
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
419
605
|
* @param tipArchive - The archive to check
|
|
420
606
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
421
607
|
*/
|
|
422
|
-
public
|
|
608
|
+
public async canProposeAt(
|
|
423
609
|
tipArchive: Fr,
|
|
424
610
|
msgSender: EthAddress,
|
|
425
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
611
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
|
|
426
612
|
) {
|
|
427
613
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
428
614
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
429
615
|
|
|
616
|
+
const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
|
|
617
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
618
|
+
const nextL1SlotTs = (await this.getNextL1SlotTimestampWithL1Floor()) + slotOffset;
|
|
619
|
+
|
|
430
620
|
return this.rollupContract
|
|
431
|
-
.
|
|
621
|
+
.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
432
622
|
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
433
623
|
})
|
|
434
624
|
.catch(err => {
|
|
@@ -442,6 +632,7 @@ export class SequencerPublisher {
|
|
|
442
632
|
return undefined;
|
|
443
633
|
});
|
|
444
634
|
}
|
|
635
|
+
|
|
445
636
|
/**
|
|
446
637
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
447
638
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
@@ -465,7 +656,7 @@ export class SequencerPublisher {
|
|
|
465
656
|
flags,
|
|
466
657
|
] as const;
|
|
467
658
|
|
|
468
|
-
const ts =
|
|
659
|
+
const ts = await this.getNextL1SlotTimestampWithL1Floor();
|
|
469
660
|
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
470
661
|
opts?.forcePendingCheckpointNumber,
|
|
471
662
|
);
|
|
@@ -521,6 +712,8 @@ export class SequencerPublisher {
|
|
|
521
712
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
713
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
523
714
|
|
|
715
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
716
|
+
|
|
524
717
|
try {
|
|
525
718
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
719
|
request,
|
|
@@ -572,6 +765,18 @@ export class SequencerPublisher {
|
|
|
572
765
|
|
|
573
766
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
767
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
768
|
+
this.backupFailedTx({
|
|
769
|
+
id: keccak256(request.data!),
|
|
770
|
+
failureType: 'simulation',
|
|
771
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
772
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
773
|
+
error: { message: viemError.message, name: viemError.name },
|
|
774
|
+
context: {
|
|
775
|
+
actions: [`invalidate-${reason}`],
|
|
776
|
+
checkpointNumber,
|
|
777
|
+
sender: this.getSenderAddress().toString(),
|
|
778
|
+
},
|
|
779
|
+
});
|
|
575
780
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
576
781
|
}
|
|
577
782
|
}
|
|
@@ -616,25 +821,11 @@ export class SequencerPublisher {
|
|
|
616
821
|
attestationsAndSignersSignature: Signature,
|
|
617
822
|
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
618
823
|
): Promise<bigint> {
|
|
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
|
-
|
|
824
|
+
// Anchor the simulation timestamp to the checkpoint's own slot start time
|
|
825
|
+
// rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
|
|
826
|
+
const ts = checkpoint.header.timestamp;
|
|
636
827
|
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
828
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
638
829
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
830
|
|
|
640
831
|
const args = [
|
|
@@ -642,7 +833,7 @@ export class SequencerPublisher {
|
|
|
642
833
|
header: checkpoint.header.toViem(),
|
|
643
834
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
835
|
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier:
|
|
836
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
646
837
|
},
|
|
647
838
|
},
|
|
648
839
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -691,6 +882,32 @@ export class SequencerPublisher {
|
|
|
691
882
|
return false;
|
|
692
883
|
}
|
|
693
884
|
|
|
885
|
+
// Check if payload was already submitted to governance
|
|
886
|
+
const cacheKey = payload.toString();
|
|
887
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
888
|
+
try {
|
|
889
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
890
|
+
const proposed = await retry(
|
|
891
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
892
|
+
'Check if payload was proposed',
|
|
893
|
+
makeBackoff([0, 1, 2]),
|
|
894
|
+
this.log,
|
|
895
|
+
true,
|
|
896
|
+
);
|
|
897
|
+
if (proposed) {
|
|
898
|
+
this.payloadProposedCache.add(cacheKey);
|
|
899
|
+
}
|
|
900
|
+
} catch (err) {
|
|
901
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
907
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
|
|
694
911
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
912
|
this.lastActions[signalType] = slotNumber;
|
|
696
913
|
const action = signalType;
|
|
@@ -709,11 +926,26 @@ export class SequencerPublisher {
|
|
|
709
926
|
lastValidL2Slot: slotNumber,
|
|
710
927
|
});
|
|
711
928
|
|
|
929
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
930
|
+
|
|
712
931
|
try {
|
|
713
932
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
714
933
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
715
934
|
} catch (err) {
|
|
716
|
-
|
|
935
|
+
const viemError = formatViemError(err);
|
|
936
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
937
|
+
this.backupFailedTx({
|
|
938
|
+
id: keccak256(request.data!),
|
|
939
|
+
failureType: 'simulation',
|
|
940
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
941
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
942
|
+
error: { message: viemError.message, name: viemError.name },
|
|
943
|
+
context: {
|
|
944
|
+
actions: [action],
|
|
945
|
+
slot: slotNumber,
|
|
946
|
+
sender: this.getSenderAddress().toString(),
|
|
947
|
+
},
|
|
948
|
+
});
|
|
717
949
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
718
950
|
}
|
|
719
951
|
|
|
@@ -918,14 +1150,15 @@ export class SequencerPublisher {
|
|
|
918
1150
|
const checkpointHeader = checkpoint.header;
|
|
919
1151
|
|
|
920
1152
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1153
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
1154
|
|
|
923
|
-
const proposeTxArgs = {
|
|
1155
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
1156
|
header: checkpointHeader,
|
|
925
1157
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
1158
|
blobs,
|
|
927
1159
|
attestationsAndSigners,
|
|
928
1160
|
attestationsAndSignersSignature,
|
|
1161
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
1162
|
};
|
|
930
1163
|
|
|
931
1164
|
let ts: bigint;
|
|
@@ -1008,6 +1241,8 @@ export class SequencerPublisher {
|
|
|
1008
1241
|
|
|
1009
1242
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1010
1243
|
|
|
1244
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1245
|
+
|
|
1011
1246
|
let gasUsed: bigint;
|
|
1012
1247
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1013
1248
|
try {
|
|
@@ -1017,6 +1252,19 @@ export class SequencerPublisher {
|
|
|
1017
1252
|
const viemError = formatViemError(err, simulateAbi);
|
|
1018
1253
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
1254
|
|
|
1255
|
+
this.backupFailedTx({
|
|
1256
|
+
id: keccak256(request.data!),
|
|
1257
|
+
failureType: 'simulation',
|
|
1258
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1259
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1260
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1261
|
+
context: {
|
|
1262
|
+
actions: [action],
|
|
1263
|
+
slot: slotNumber,
|
|
1264
|
+
sender: this.getSenderAddress().toString(),
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1020
1268
|
return false;
|
|
1021
1269
|
}
|
|
1022
1270
|
|
|
@@ -1100,9 +1348,27 @@ export class SequencerPublisher {
|
|
|
1100
1348
|
kzg,
|
|
1101
1349
|
},
|
|
1102
1350
|
)
|
|
1103
|
-
.catch(err => {
|
|
1104
|
-
const
|
|
1105
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1351
|
+
.catch(async err => {
|
|
1352
|
+
const viemError = formatViemError(err);
|
|
1353
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1354
|
+
const validateBlobsData = encodeFunctionData({
|
|
1355
|
+
abi: RollupAbi,
|
|
1356
|
+
functionName: 'validateBlobs',
|
|
1357
|
+
args: [blobInput],
|
|
1358
|
+
});
|
|
1359
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1360
|
+
this.backupFailedTx({
|
|
1361
|
+
id: keccak256(validateBlobsData),
|
|
1362
|
+
failureType: 'simulation',
|
|
1363
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1364
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1365
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1366
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1367
|
+
context: {
|
|
1368
|
+
actions: ['validate-blobs'],
|
|
1369
|
+
sender: this.getSenderAddress().toString(),
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
1106
1372
|
throw new Error('Failed to validate blobs');
|
|
1107
1373
|
});
|
|
1108
1374
|
}
|
|
@@ -1113,8 +1379,7 @@ export class SequencerPublisher {
|
|
|
1113
1379
|
header: encodedData.header.toViem(),
|
|
1114
1380
|
archive: toHex(encodedData.archive),
|
|
1115
1381
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1382
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1383
|
},
|
|
1119
1384
|
},
|
|
1120
1385
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1140,7 +1405,7 @@ export class SequencerPublisher {
|
|
|
1140
1405
|
readonly header: ViemHeader;
|
|
1141
1406
|
readonly archive: `0x${string}`;
|
|
1142
1407
|
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier:
|
|
1408
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1144
1409
|
};
|
|
1145
1410
|
},
|
|
1146
1411
|
ViemCommitteeAttestations,
|
|
@@ -1182,6 +1447,8 @@ export class SequencerPublisher {
|
|
|
1182
1447
|
});
|
|
1183
1448
|
}
|
|
1184
1449
|
|
|
1450
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1451
|
+
|
|
1185
1452
|
const simulationResult = await this.l1TxUtils
|
|
1186
1453
|
.simulate(
|
|
1187
1454
|
{
|
|
@@ -1215,6 +1482,18 @@ export class SequencerPublisher {
|
|
|
1215
1482
|
};
|
|
1216
1483
|
}
|
|
1217
1484
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1485
|
+
this.backupFailedTx({
|
|
1486
|
+
id: keccak256(rollupData),
|
|
1487
|
+
failureType: 'simulation',
|
|
1488
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1489
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1490
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1491
|
+
context: {
|
|
1492
|
+
actions: ['propose'],
|
|
1493
|
+
slot: Number(args[0].header.slotNumber),
|
|
1494
|
+
sender: this.getSenderAddress().toString(),
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1218
1497
|
throw err;
|
|
1219
1498
|
});
|
|
1220
1499
|
|
|
@@ -1310,4 +1589,21 @@ export class SequencerPublisher {
|
|
|
1310
1589
|
},
|
|
1311
1590
|
});
|
|
1312
1591
|
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Returns the timestamp to use when simulating L1 proposal calls.
|
|
1595
|
+
* Uses the wall-clock-based next L1 slot boundary, but floors it with the latest L1 block timestamp
|
|
1596
|
+
* plus one slot duration. This prevents the sequencer from targeting a future L2 slot when the L1
|
|
1597
|
+
* chain hasn't caught up to the wall clock yet (e.g., the dateProvider is one L1 slot ahead of the
|
|
1598
|
+
* latest mined block), which would cause the propose tx to land in an L1 block with block.timestamp
|
|
1599
|
+
* still in the previous L2 slot.
|
|
1600
|
+
* TODO(palla): Properly fix by keeping dateProvider synced with anvil's chain time on every block.
|
|
1601
|
+
*/
|
|
1602
|
+
private async getNextL1SlotTimestampWithL1Floor(): Promise<bigint> {
|
|
1603
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1604
|
+
const fromWallClock = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1605
|
+
const latestBlock = await this.l1TxUtils.client.getBlock();
|
|
1606
|
+
const fromL1Block = latestBlock.timestamp + BigInt(l1Constants.ethereumSlotDuration);
|
|
1607
|
+
return fromWallClock > fromL1Block ? fromWallClock : fromL1Block;
|
|
1608
|
+
}
|
|
1313
1609
|
}
|