@aztec/sequencer-client 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d
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/README.md +283 -21
- package/dest/client/sequencer-client.d.ts +20 -5
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +26 -22
- package/dest/config.d.ts +13 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +67 -26
- 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 +138 -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 +80 -0
- package/dest/global_variable_builder/global_builder.d.ts +7 -26
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +6 -67
- 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 +7 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +13 -3
- package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
- package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
- package/dest/publisher/l1_to_l2_messaging.js +70 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
- package/dest/publisher/sequencer-bundle-simulator.js +198 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +0 -1
- package/dest/publisher/sequencer-publisher.d.ts +65 -70
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +413 -559
- package/dest/publisher/write_json.d.ts +11 -0
- package/dest/publisher/write_json.d.ts.map +1 -0
- package/dest/publisher/write_json.js +57 -0
- package/dest/sequencer/automine/automine_factory.d.ts +56 -0
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_factory.js +85 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_sequencer.js +696 -0
- package/dest/sequencer/automine/index.d.ts +3 -0
- package/dest/sequencer/automine/index.d.ts.map +1 -0
- package/dest/sequencer/automine/index.js +2 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +67 -36
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +705 -256
- 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/errors.d.ts +1 -8
- package/dest/sequencer/errors.d.ts.map +1 -1
- package/dest/sequencer/errors.js +0 -9
- package/dest/sequencer/events.d.ts +61 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +10 -11
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +34 -20
- package/dest/sequencer/requests_tracker.d.ts +22 -0
- package/dest/sequencer/requests_tracker.d.ts.map +1 -0
- package/dest/sequencer/requests_tracker.js +33 -0
- package/dest/sequencer/sequencer.d.ts +147 -33
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +543 -180
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -3
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/utils.d.ts +15 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +25 -7
- package/package.json +28 -27
- package/src/client/sequencer-client.ts +37 -27
- package/src/config.ts +79 -26
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +182 -0
- package/src/global_variable_builder/fee_provider.ts +97 -0
- package/src/global_variable_builder/global_builder.ts +11 -91
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +30 -7
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +458 -622
- package/src/publisher/write_json.ts +78 -0
- package/src/sequencer/automine/README.md +60 -0
- package/src/sequencer/automine/automine_factory.ts +152 -0
- package/src/sequencer/automine/automine_sequencer.ts +800 -0
- package/src/sequencer/automine/index.ts +6 -0
- package/src/sequencer/checkpoint_proposal_job.ts +823 -316
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/errors.ts +0 -15
- package/src/sequencer/events.ts +66 -5
- package/src/sequencer/metrics.ts +49 -25
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +607 -206
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -2
- package/src/test/utils.ts +61 -10
- package/dest/sequencer/timetable.d.ts +0 -88
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -222
- package/src/sequencer/README.md +0 -531
- package/src/sequencer/timetable.ts +0 -283
|
@@ -3,17 +3,16 @@ 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
|
-
type FeeHeader,
|
|
9
7
|
type GovernanceProposerContract,
|
|
10
|
-
type IEmpireBase,
|
|
11
8
|
MULTI_CALL_3_ADDRESS,
|
|
12
9
|
Multicall3,
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type
|
|
10
|
+
MulticallForwarderRevertedError,
|
|
11
|
+
type PayloadProposalStatus,
|
|
12
|
+
type RollupContract,
|
|
13
|
+
type SimulationOverridesPlan,
|
|
14
|
+
type SlashingProposerContract,
|
|
15
|
+
buildSimulationOverridesStateOverride,
|
|
17
16
|
} from '@aztec/ethereum/contracts';
|
|
18
17
|
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
19
18
|
import {
|
|
@@ -25,50 +24,72 @@ import {
|
|
|
25
24
|
type TransactionStats,
|
|
26
25
|
WEI_CONST,
|
|
27
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
import {
|
|
28
|
+
FormattedViemError,
|
|
29
|
+
formatViemError,
|
|
30
|
+
mergeAbis,
|
|
31
|
+
tryDecodeRevertReason,
|
|
32
|
+
tryExtractEvent,
|
|
33
|
+
} from '@aztec/ethereum/utils';
|
|
31
34
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
32
35
|
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
33
36
|
import { pick } from '@aztec/foundation/collection';
|
|
34
37
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
38
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
36
39
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
37
|
-
import { Signature
|
|
40
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
38
41
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
39
|
-
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
42
|
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
41
43
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
42
44
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
43
|
-
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
45
|
+
import { EmpireBaseAbi, ErrorsAbi, RollupAbi, SlashingProposerAbi } from '@aztec/l1-artifacts';
|
|
44
46
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
45
47
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
46
48
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
+
import type { SequencerConfig } from '@aztec/stdlib/config';
|
|
50
|
+
import {
|
|
51
|
+
getLastL1SlotTimestampForL2Slot,
|
|
52
|
+
getNextL1SlotTimestamp,
|
|
53
|
+
getTimestampForSlot,
|
|
54
|
+
} from '@aztec/stdlib/epoch-helpers';
|
|
49
55
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
50
56
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
51
57
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
52
58
|
|
|
53
59
|
import {
|
|
60
|
+
type Abi,
|
|
54
61
|
type Hex,
|
|
55
|
-
type StateOverride,
|
|
56
62
|
type TransactionReceipt,
|
|
57
63
|
type TypedDataDefinition,
|
|
58
64
|
encodeFunctionData,
|
|
59
65
|
keccak256,
|
|
60
|
-
multicall3Abi,
|
|
61
66
|
toHex,
|
|
62
67
|
} from 'viem';
|
|
63
68
|
|
|
64
69
|
import type { SequencerPublisherConfig } from './config.js';
|
|
65
70
|
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
71
|
+
import { type DroppedRequest, SequencerBundleSimulator } from './sequencer-bundle-simulator.js';
|
|
66
72
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
67
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Returns true if the receipt indicates a successful send AND the expected event was emitted
|
|
76
|
+
* by the target contract. Both pieces are required: an aggregate3 entry that reverted will
|
|
77
|
+
* have receipt.status === 'success' but no event log.
|
|
78
|
+
*/
|
|
79
|
+
function extractEventSuccess(
|
|
80
|
+
receipt: TransactionReceipt | undefined,
|
|
81
|
+
opts: { address: string; abi: Abi; eventName: string },
|
|
82
|
+
): boolean {
|
|
83
|
+
if (!receipt || receipt.status !== 'success') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return !!tryExtractEvent(receipt.logs, opts.address.toString() as Hex, opts.abi, opts.eventName);
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
/** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
|
|
69
90
|
export type SendRequestsResult = {
|
|
70
|
-
/** The L1 transaction receipt
|
|
71
|
-
result: { receipt: TransactionReceipt
|
|
91
|
+
/** The L1 transaction receipt from the bundled multicall. */
|
|
92
|
+
result: { receipt: TransactionReceipt };
|
|
72
93
|
/** Actions that expired (past their deadline) before the request was sent. */
|
|
73
94
|
expiredActions: Action[];
|
|
74
95
|
/** Actions that were included in the sent L1 transaction. */
|
|
@@ -98,18 +119,16 @@ type L1ProcessArgs = {
|
|
|
98
119
|
export const Actions = [
|
|
99
120
|
'invalidate-by-invalid-attestation',
|
|
100
121
|
'invalidate-by-insufficient-attestations',
|
|
122
|
+
'prune',
|
|
101
123
|
'propose',
|
|
102
124
|
'governance-signal',
|
|
103
|
-
'empire-slashing-signal',
|
|
104
|
-
'create-empire-payload',
|
|
105
|
-
'execute-empire-payload',
|
|
106
125
|
'vote-offenses',
|
|
107
126
|
'execute-slash',
|
|
108
127
|
] as const;
|
|
109
128
|
|
|
110
129
|
export type Action = (typeof Actions)[number];
|
|
111
130
|
|
|
112
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
131
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
113
132
|
|
|
114
133
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
115
134
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -124,12 +143,18 @@ export type InvalidateCheckpointRequest = {
|
|
|
124
143
|
lastArchive: Fr;
|
|
125
144
|
};
|
|
126
145
|
|
|
127
|
-
|
|
146
|
+
type EnqueueProposeCheckpointOpts = {
|
|
147
|
+
txTimeoutAt?: Date;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export interface RequestWithExpiry {
|
|
128
151
|
action: Action;
|
|
129
152
|
request: L1TxRequest;
|
|
130
153
|
lastValidL2Slot: SlotNumber;
|
|
131
154
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
132
155
|
blobConfig?: L1BlobInputs;
|
|
156
|
+
/** Gas consumed by validateBlobs; stashed for the bundle simulate at send time. */
|
|
157
|
+
blobEvaluationGas?: bigint;
|
|
133
158
|
checkSuccess: (
|
|
134
159
|
request: L1TxRequest,
|
|
135
160
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -139,29 +164,30 @@ interface RequestWithExpiry {
|
|
|
139
164
|
export class SequencerPublisher {
|
|
140
165
|
private interrupted = false;
|
|
141
166
|
private metrics: SequencerPublisherMetrics;
|
|
167
|
+
private bundleSimulator: SequencerBundleSimulator;
|
|
142
168
|
public epochCache: EpochCache;
|
|
143
169
|
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
144
170
|
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
/**
|
|
172
|
+
* ABI used to decode raw revert payloads from dropped bundle entries when the original
|
|
173
|
+
* request did not carry an abi (e.g. the propose request). Merges every contract the
|
|
174
|
+
* publisher can route to so any of their custom errors decode against it.
|
|
175
|
+
*/
|
|
176
|
+
private readonly revertDecoderAbi: Abi = mergeAbis([RollupAbi, SlashingProposerAbi, EmpireBaseAbi, ErrorsAbi]);
|
|
147
177
|
|
|
148
178
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
149
179
|
|
|
150
|
-
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
151
|
-
private payloadProposedCache: Set<string> = new Set<string>();
|
|
152
|
-
|
|
153
180
|
protected log: Logger;
|
|
154
181
|
protected ethereumSlotDuration: bigint;
|
|
155
182
|
protected aztecSlotDuration: bigint;
|
|
183
|
+
private readonly previousL1BlockWaitTimeoutMs: number;
|
|
184
|
+
private readonly previousL1BlockWaitPollIntervalMs: number;
|
|
156
185
|
|
|
157
186
|
/** Date provider for wall-clock time. */
|
|
158
187
|
private readonly dateProvider: DateProvider;
|
|
159
188
|
|
|
160
189
|
private blobClient: BlobClientInterface;
|
|
161
190
|
|
|
162
|
-
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
163
|
-
private proposerAddressForSimulation?: EthAddress;
|
|
164
|
-
|
|
165
191
|
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
166
192
|
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
167
193
|
|
|
@@ -174,33 +200,32 @@ export class SequencerPublisher {
|
|
|
174
200
|
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
175
201
|
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
176
202
|
|
|
177
|
-
// A CALL to a cold address is 2700 gas
|
|
178
|
-
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
179
|
-
|
|
180
|
-
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
181
|
-
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
182
|
-
|
|
183
203
|
public l1TxUtils: L1TxUtils;
|
|
184
204
|
public rollupContract: RollupContract;
|
|
185
205
|
public govProposerContract: GovernanceProposerContract;
|
|
186
|
-
public slashingProposerContract:
|
|
187
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
206
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
188
207
|
|
|
189
208
|
public readonly tracer: Tracer;
|
|
190
209
|
|
|
191
210
|
protected requests: RequestWithExpiry[] = [];
|
|
192
211
|
|
|
193
212
|
constructor(
|
|
194
|
-
private config: Pick<
|
|
213
|
+
private config: Pick<
|
|
214
|
+
SequencerPublisherConfig,
|
|
215
|
+
| 'fishermanMode'
|
|
216
|
+
| 'l1TxFailedStore'
|
|
217
|
+
| 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
|
|
218
|
+
| 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
|
|
219
|
+
> &
|
|
220
|
+
Pick<SequencerConfig, 'governanceProposerForcePayloadVote'> &
|
|
195
221
|
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
196
222
|
deps: {
|
|
197
223
|
telemetry?: TelemetryClient;
|
|
198
224
|
blobClient: BlobClientInterface;
|
|
199
225
|
l1TxUtils: L1TxUtils;
|
|
200
226
|
rollupContract: RollupContract;
|
|
201
|
-
slashingProposerContract:
|
|
227
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
202
228
|
governanceProposerContract: GovernanceProposerContract;
|
|
203
|
-
slashFactoryContract: SlashFactoryContract;
|
|
204
229
|
epochCache: EpochCache;
|
|
205
230
|
dateProvider: DateProvider;
|
|
206
231
|
metrics: SequencerPublisherMetrics;
|
|
@@ -212,6 +237,8 @@ export class SequencerPublisher {
|
|
|
212
237
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
213
238
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
214
239
|
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
240
|
+
this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
|
|
241
|
+
this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
|
|
215
242
|
this.dateProvider = deps.dateProvider;
|
|
216
243
|
this.epochCache = deps.epochCache;
|
|
217
244
|
this.lastActions = deps.lastActions;
|
|
@@ -235,14 +262,12 @@ export class SequencerPublisher {
|
|
|
235
262
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
236
263
|
this.slashingProposerContract = newSlashingProposer;
|
|
237
264
|
});
|
|
238
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
239
|
-
|
|
240
265
|
// Initialize L1 fee analyzer for fisherman mode
|
|
241
266
|
if (config.fishermanMode) {
|
|
242
267
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
243
268
|
this.l1TxUtils.client,
|
|
244
269
|
deps.dateProvider,
|
|
245
|
-
|
|
270
|
+
this.log.createChild('fee-analyzer'),
|
|
246
271
|
);
|
|
247
272
|
}
|
|
248
273
|
|
|
@@ -250,11 +275,18 @@ export class SequencerPublisher {
|
|
|
250
275
|
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
251
276
|
this.l1TxUtils.client,
|
|
252
277
|
this.rollupContract,
|
|
253
|
-
|
|
278
|
+
this.log.createChild('price-oracle'),
|
|
254
279
|
);
|
|
255
280
|
|
|
256
281
|
// Initialize failed L1 tx store (optional, for test networks)
|
|
257
282
|
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
283
|
+
|
|
284
|
+
this.bundleSimulator = new SequencerBundleSimulator({
|
|
285
|
+
getL1TxUtils: () => this.l1TxUtils,
|
|
286
|
+
rollupContract: this.rollupContract,
|
|
287
|
+
epochCache: this.epochCache,
|
|
288
|
+
log: this.log.createChild('bundle-simulator'),
|
|
289
|
+
});
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
/**
|
|
@@ -285,10 +317,14 @@ export class SequencerPublisher {
|
|
|
285
317
|
|
|
286
318
|
/**
|
|
287
319
|
* Gets the fee asset price modifier from the oracle.
|
|
288
|
-
*
|
|
320
|
+
*
|
|
321
|
+
* @param predictedParentEthPerFeeAssetE12 - Optional predicted parent eth-per-fee-asset (E12).
|
|
322
|
+
* Pipelined proposers should pass the value from the predicted parent fee header so the
|
|
323
|
+
* modifier matches the parent L1 will use when applying it.
|
|
324
|
+
* @returns The fee asset price modifier in basis points, or 0n if the oracle query fails.
|
|
289
325
|
*/
|
|
290
|
-
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
291
|
-
return this.feeAssetPriceOracle.computePriceModifier();
|
|
326
|
+
public getFeeAssetPriceModifier(predictedParentEthPerFeeAssetE12?: bigint): Promise<bigint> {
|
|
327
|
+
return this.feeAssetPriceOracle.computePriceModifier(predictedParentEthPerFeeAssetE12);
|
|
292
328
|
}
|
|
293
329
|
|
|
294
330
|
public getSenderAddress() {
|
|
@@ -302,14 +338,6 @@ export class SequencerPublisher {
|
|
|
302
338
|
return this.l1FeeAnalyzer;
|
|
303
339
|
}
|
|
304
340
|
|
|
305
|
-
/**
|
|
306
|
-
* Sets the proposer address to use for simulations in fisherman mode.
|
|
307
|
-
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
308
|
-
*/
|
|
309
|
-
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
310
|
-
this.proposerAddressForSimulation = proposerAddress;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
341
|
public addRequest(request: RequestWithExpiry) {
|
|
314
342
|
this.requests.push(request);
|
|
315
343
|
}
|
|
@@ -387,23 +415,26 @@ export class SequencerPublisher {
|
|
|
387
415
|
|
|
388
416
|
/**
|
|
389
417
|
* Sends all requests that are still valid.
|
|
418
|
+
* @param targetSlot - The target L2 slot for this send. When provided (the production path, via
|
|
419
|
+
* sendRequestsAt), it is threaded into bundleSimulate so the block.timestamp override matches
|
|
420
|
+
* the slot the propose is built for. When omitted, falls back to getCurrentL2Slot() for the
|
|
421
|
+
* AutomineSequencer, which publishes synchronously within the current slot.
|
|
390
422
|
* @returns one of:
|
|
391
423
|
* - A receipt and stats if the tx succeeded
|
|
392
424
|
* - a receipt and errorMsg if it failed on L1
|
|
393
425
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
394
426
|
*/
|
|
395
427
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
396
|
-
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
428
|
+
public async sendRequests(targetSlot?: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
397
429
|
const requestsToProcess = [...this.requests];
|
|
398
430
|
this.requests = [];
|
|
399
431
|
|
|
400
432
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
401
433
|
return undefined;
|
|
402
434
|
}
|
|
403
|
-
const currentL2Slot = this.getCurrentL2Slot();
|
|
435
|
+
const currentL2Slot = targetSlot ?? this.getCurrentL2Slot();
|
|
404
436
|
this.log.debug(`Sending requests on L2 slot ${currentL2Slot}`);
|
|
405
437
|
const validRequests = requestsToProcess.filter(request => request.lastValidL2Slot >= currentL2Slot);
|
|
406
|
-
const validActions = validRequests.map(x => x.action);
|
|
407
438
|
const expiredActions = requestsToProcess
|
|
408
439
|
.filter(request => request.lastValidL2Slot < currentL2Slot)
|
|
409
440
|
.map(x => x.action);
|
|
@@ -426,70 +457,60 @@ export class SequencerPublisher {
|
|
|
426
457
|
return undefined;
|
|
427
458
|
}
|
|
428
459
|
|
|
429
|
-
//
|
|
430
|
-
// find requests with gas and blob configs
|
|
431
|
-
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
460
|
+
// Collect earliest txTimeoutAt across all requests.
|
|
432
461
|
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
433
|
-
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
434
|
-
|
|
435
|
-
if (blobConfigs.length > 1) {
|
|
436
|
-
throw new Error('Multiple blob configs found');
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const blobConfig = blobConfigs[0];
|
|
440
|
-
|
|
441
|
-
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
442
|
-
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
443
|
-
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
444
|
-
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
445
|
-
const maxGas = MAX_L1_TX_LIMIT;
|
|
446
|
-
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
447
|
-
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
448
|
-
requested: gasLimit,
|
|
449
|
-
capped: maxGas,
|
|
450
|
-
});
|
|
451
|
-
gasLimit = maxGas;
|
|
452
|
-
}
|
|
453
462
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
454
|
-
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined;
|
|
455
|
-
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
463
|
+
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined;
|
|
456
464
|
|
|
457
465
|
// Sort the requests so that proposals always go first
|
|
458
466
|
// This ensures the committee gets precomputed correctly
|
|
459
467
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
460
468
|
|
|
461
469
|
try {
|
|
462
|
-
//
|
|
463
|
-
const
|
|
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;
|
|
470
|
+
// Bundle-level eth_simulateV1: filters out entries that revert and derives the gasLimit.
|
|
471
|
+
const bundleResult = await this.bundleSimulator.simulate(validRequests, currentL2Slot);
|
|
476
472
|
|
|
477
|
-
|
|
473
|
+
if (bundleResult.kind === 'aborted') {
|
|
474
|
+
this.logDroppedInSim(bundleResult.droppedRequests);
|
|
475
|
+
void this.backupDroppedInSim(bundleResult.droppedRequests).catch(err =>
|
|
476
|
+
this.log.error(`Failed to backup requests dropped in simulation`, err),
|
|
477
|
+
);
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const { requests, droppedRequests, gasLimit } =
|
|
482
|
+
bundleResult.kind === 'fallback'
|
|
483
|
+
? {
|
|
484
|
+
requests: bundleResult.requests,
|
|
485
|
+
droppedRequests: bundleResult.droppedRequests,
|
|
486
|
+
gasLimit: MAX_L1_TX_LIMIT,
|
|
487
|
+
}
|
|
488
|
+
: bundleResult;
|
|
489
|
+
|
|
490
|
+
this.logDroppedInSim(droppedRequests);
|
|
491
|
+
|
|
492
|
+
// Compute blobConfig from survivors (not original validRequests) so that if the propose
|
|
493
|
+
// entry was dropped by bundleSimulate we don't attach a blob-typed config to a non-blob tx.
|
|
494
|
+
const [blobConfig] = requests.filter(r => r.blobConfig).map(r => r.blobConfig);
|
|
495
|
+
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
478
496
|
|
|
479
497
|
this.log.debug('Forwarding transactions', {
|
|
480
|
-
|
|
498
|
+
requests: requests.map(request => request.action),
|
|
481
499
|
txConfig,
|
|
482
500
|
});
|
|
483
|
-
const result = await this.forwardWithPublisherRotation(
|
|
501
|
+
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
|
|
484
502
|
if (result === undefined) {
|
|
485
503
|
return undefined;
|
|
486
504
|
}
|
|
487
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
488
|
-
|
|
505
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(requests, result);
|
|
506
|
+
const allFailedActions = [...failedActions, ...droppedRequests.map(d => d.request.action)];
|
|
507
|
+
return {
|
|
489
508
|
result,
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
509
|
+
expiredActions,
|
|
510
|
+
sentActions: requests.map(x => x.action),
|
|
511
|
+
successfulActions,
|
|
512
|
+
failedActions: allFailedActions,
|
|
513
|
+
};
|
|
493
514
|
} catch (err) {
|
|
494
515
|
const viemError = formatViemError(err);
|
|
495
516
|
this.log.error(`Failed to publish bundled transactions`, viemError);
|
|
@@ -506,6 +527,40 @@ export class SequencerPublisher {
|
|
|
506
527
|
}
|
|
507
528
|
}
|
|
508
529
|
|
|
530
|
+
/** Logs entries dropped by bundle simulation as warnings on the publisher's logger. */
|
|
531
|
+
private logDroppedInSim(dropped: DroppedRequest[]): void {
|
|
532
|
+
for (const drop of dropped) {
|
|
533
|
+
const revertReasonDecoded = drop.revertReason ?? tryDecodeRevertReason(drop.returnData, this.revertDecoderAbi);
|
|
534
|
+
this.log.warn('Bundle entry dropped: action reverted in sim', {
|
|
535
|
+
action: drop.request.action,
|
|
536
|
+
revertReason: revertReasonDecoded ?? drop.returnData,
|
|
537
|
+
revertReasonDecoded,
|
|
538
|
+
returnData: drop.returnData,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Backs up entries dropped by bundle simulation, one record per dropped action. */
|
|
544
|
+
private async backupDroppedInSim(dropped: DroppedRequest[]): Promise<void> {
|
|
545
|
+
if (dropped.length === 0) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
549
|
+
for (const { request: req } of dropped) {
|
|
550
|
+
this.backupFailedTx({
|
|
551
|
+
id: keccak256(req.request.data!),
|
|
552
|
+
failureType: 'simulation',
|
|
553
|
+
request: { to: req.request.to! as Hex, data: req.request.data! },
|
|
554
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
555
|
+
error: { message: 'Bundle entry dropped: action reverted in sim' },
|
|
556
|
+
context: {
|
|
557
|
+
actions: [req.action],
|
|
558
|
+
sender: this.getSenderAddress().toString(),
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
509
564
|
/**
|
|
510
565
|
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
511
566
|
* failure occurs (i.e. the tx never reached the chain).
|
|
@@ -516,19 +571,30 @@ export class SequencerPublisher {
|
|
|
516
571
|
txConfig: RequestWithExpiry['gasConfig'],
|
|
517
572
|
blobConfig: L1BlobInputs | undefined,
|
|
518
573
|
) {
|
|
574
|
+
if (!txConfig?.gasLimit) {
|
|
575
|
+
throw new Error('gasLimit is required for bundled transactions');
|
|
576
|
+
}
|
|
577
|
+
const txConfigWithGasLimit = txConfig as L1TxConfig & { gasLimit: bigint };
|
|
578
|
+
|
|
519
579
|
const triedAddresses: EthAddress[] = [];
|
|
520
580
|
let currentPublisher = this.l1TxUtils;
|
|
521
581
|
|
|
522
582
|
while (true) {
|
|
583
|
+
if (txConfig.txTimeoutAt && new Date() > txConfig.txTimeoutAt) {
|
|
584
|
+
this.log.warn(`Tx timeout (${txConfig.txTimeoutAt.toISOString()}) elapsed; stopping publisher rotation`, {
|
|
585
|
+
triedAddresses: triedAddresses.map(a => a.toString()),
|
|
586
|
+
});
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
523
589
|
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
590
|
+
|
|
524
591
|
try {
|
|
525
592
|
const result = await Multicall3.forward(
|
|
526
593
|
validRequests.map(r => r.request),
|
|
527
594
|
currentPublisher,
|
|
528
|
-
|
|
595
|
+
txConfigWithGasLimit,
|
|
529
596
|
blobConfig,
|
|
530
|
-
|
|
531
|
-
this.log,
|
|
597
|
+
{ gasLimitRequired: true },
|
|
532
598
|
);
|
|
533
599
|
this.l1TxUtils = currentPublisher;
|
|
534
600
|
return result;
|
|
@@ -536,6 +602,12 @@ export class SequencerPublisher {
|
|
|
536
602
|
if (err instanceof TimeoutError) {
|
|
537
603
|
throw err;
|
|
538
604
|
}
|
|
605
|
+
if (err instanceof MulticallForwarderRevertedError) {
|
|
606
|
+
this.log.error('Forwarder transaction reverted on-chain; not rotating publisher', err, {
|
|
607
|
+
transactionHash: err.receipt.transactionHash,
|
|
608
|
+
});
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
539
611
|
const viemError = formatViemError(err);
|
|
540
612
|
if (!this.getNextPublisher) {
|
|
541
613
|
this.log.error('Failed to publish bundled transactions', viemError);
|
|
@@ -547,7 +619,11 @@ export class SequencerPublisher {
|
|
|
547
619
|
);
|
|
548
620
|
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
549
621
|
if (!nextPublisher) {
|
|
550
|
-
this.log.error(
|
|
622
|
+
this.log.error(
|
|
623
|
+
`All available publishers exhausted (tried ${triedAddresses.length}), failed to publish bundled transactions`,
|
|
624
|
+
viemError,
|
|
625
|
+
{ triedAddresses: triedAddresses.map(a => a.toString()) },
|
|
626
|
+
);
|
|
551
627
|
return undefined;
|
|
552
628
|
}
|
|
553
629
|
currentPublisher = nextPublisher;
|
|
@@ -556,90 +632,97 @@ export class SequencerPublisher {
|
|
|
556
632
|
}
|
|
557
633
|
|
|
558
634
|
/*
|
|
559
|
-
* Schedules sending all enqueued requests at (or after) the given
|
|
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).
|
|
635
|
+
* Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
|
|
562
636
|
*/
|
|
563
|
-
public async sendRequestsAt(
|
|
564
|
-
|
|
565
|
-
if (ms > 0) {
|
|
566
|
-
this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
|
|
567
|
-
await this.interruptibleSleep.sleep(ms);
|
|
568
|
-
}
|
|
637
|
+
public async sendRequestsAt(targetSlot: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
638
|
+
await this.waitForTargetSlot(targetSlot);
|
|
569
639
|
if (this.interrupted) {
|
|
570
640
|
return undefined;
|
|
571
641
|
}
|
|
572
|
-
|
|
642
|
+
|
|
643
|
+
return this.sendRequests(targetSlot);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Sleeps until one L1 slot before the L2 slot boundary, and then waits for that L1 block
|
|
648
|
+
* to be mined, so we don't risk being included in it. If that block never gets mined after
|
|
649
|
+
* a timeout, we assume it got skipped on L1, so we send the tx anyway.
|
|
650
|
+
*/
|
|
651
|
+
private async waitForTargetSlot(targetSlot: SlotNumber): Promise<void> {
|
|
652
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
653
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
654
|
+
const startOfTargetSlotTs = getTimestampForSlot(targetSlot, l1Constants);
|
|
655
|
+
const previousL1BlockTs = startOfTargetSlotTs - this.ethereumSlotDuration;
|
|
656
|
+
const waitDeadlineTs = previousL1BlockTs + BigInt(this.previousL1BlockWaitTimeoutMs / 1000);
|
|
657
|
+
const logCtx = { targetSlot, startOfTargetSlotTs, nowInSeconds, previousL1BlockTs, waitDeadlineTs };
|
|
658
|
+
|
|
659
|
+
// Check if we are already past time
|
|
660
|
+
if (nowInSeconds >= startOfTargetSlotTs) {
|
|
661
|
+
this.log.verbose(`Target slot ${targetSlot} already started, sending requests immediately`, logCtx);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Otherwise we wait
|
|
666
|
+
this.log.debug(`Waiting for slot ${targetSlot} before sending requests`, logCtx);
|
|
667
|
+
|
|
668
|
+
// Wait until previous L1 block timestamp first
|
|
669
|
+
const sleepMs = (Number(previousL1BlockTs) - nowInSeconds) * 1000;
|
|
670
|
+
if (sleepMs > 0 && !this.interrupted) {
|
|
671
|
+
this.log.trace(`Sleeping ${sleepMs}ms before waiting for previous L1 block`, logCtx);
|
|
672
|
+
await this.interruptibleSleep.sleep(sleepMs);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Then loop until we see the previous L1 block, so we know that we cannot be included in it.
|
|
676
|
+
// We time out after a while, once we are sure that that block is skipped in L1.
|
|
677
|
+
while (!this.interrupted) {
|
|
678
|
+
try {
|
|
679
|
+
const nowInSeconds = this.dateProvider.nowInSeconds();
|
|
680
|
+
logCtx.nowInSeconds = nowInSeconds;
|
|
681
|
+
|
|
682
|
+
if (nowInSeconds >= waitDeadlineTs) {
|
|
683
|
+
this.log.warn(`Timed out waiting for previous L1 block before sending requests, proceeding`, logCtx);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const latestBlockTs = await this.l1TxUtils.getBlock().then(b => b.timestamp);
|
|
688
|
+
if (latestBlockTs >= previousL1BlockTs) {
|
|
689
|
+
this.log.debug(`Previous L1 block mined, proceeding to send requests`, { ...logCtx, latestBlockTs });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
this.log.trace(`Previous L1 block not mined yet, continuing to wait`, { ...logCtx, latestBlockTs });
|
|
693
|
+
} catch (err) {
|
|
694
|
+
this.log.error(`Error while waiting for previous L1 block before sending requests; retrying`, err, logCtx);
|
|
695
|
+
} finally {
|
|
696
|
+
await this.interruptibleSleep.sleep(this.previousL1BlockWaitPollIntervalMs);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
573
699
|
}
|
|
574
700
|
|
|
575
701
|
private callbackBundledTransactions(
|
|
576
702
|
requests: RequestWithExpiry[],
|
|
577
|
-
result: { receipt: TransactionReceipt;
|
|
578
|
-
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
703
|
+
result: { receipt: TransactionReceipt; multicallData: Hex },
|
|
579
704
|
) {
|
|
580
705
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
});
|
|
608
|
-
const successfulActions: Action[] = [];
|
|
609
|
-
const failedActions: Action[] = [];
|
|
610
|
-
for (const request of requests) {
|
|
611
|
-
if (request.checkSuccess(request.request, result)) {
|
|
612
|
-
successfulActions.push(request.action);
|
|
613
|
-
} else {
|
|
614
|
-
failedActions.push(request.action);
|
|
615
|
-
}
|
|
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
|
-
});
|
|
706
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
707
|
+
result,
|
|
708
|
+
requests: requests.map(r => ({
|
|
709
|
+
...r,
|
|
710
|
+
// Avoid logging large blob data
|
|
711
|
+
blobConfig: r.blobConfig
|
|
712
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
713
|
+
: undefined,
|
|
714
|
+
})),
|
|
715
|
+
});
|
|
716
|
+
const successfulActions: Action[] = [];
|
|
717
|
+
const failedActions: Action[] = [];
|
|
718
|
+
for (const request of requests) {
|
|
719
|
+
if (request.checkSuccess(request.request, result)) {
|
|
720
|
+
successfulActions.push(request.action);
|
|
721
|
+
} else {
|
|
722
|
+
failedActions.push(request.action);
|
|
640
723
|
}
|
|
641
|
-
return { successfulActions, failedActions };
|
|
642
724
|
}
|
|
725
|
+
return { successfulActions, failedActions };
|
|
643
726
|
}
|
|
644
727
|
|
|
645
728
|
/**
|
|
@@ -647,30 +730,27 @@ export class SequencerPublisher {
|
|
|
647
730
|
* @param tipArchive - The archive to check
|
|
648
731
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
649
732
|
*/
|
|
650
|
-
public canProposeAt(
|
|
651
|
-
tipArchive: Fr,
|
|
652
|
-
msgSender: EthAddress,
|
|
653
|
-
opts: {
|
|
654
|
-
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
655
|
-
forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
|
|
656
|
-
pipelined?: boolean;
|
|
657
|
-
} = {},
|
|
658
|
-
) {
|
|
733
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
659
734
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
660
|
-
|
|
735
|
+
// These errors are expected when we cannot actually propose right now — usually because our
|
|
736
|
+
// local view of the chain is ahead of L1 (proposed parent hasn't landed yet, or someone
|
|
737
|
+
// else has just landed the slot, or the archive override doesn't match). We log a warn and
|
|
738
|
+
// skip the proposal; we do NOT treat these as bugs.
|
|
739
|
+
const expectedErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
661
740
|
|
|
662
|
-
const
|
|
663
|
-
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
741
|
+
const slotOffset = this.aztecSlotDuration;
|
|
664
742
|
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
665
743
|
|
|
666
744
|
return this.rollupContract
|
|
667
|
-
.canProposeAt(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
745
|
+
.canProposeAt(
|
|
746
|
+
tipArchive.toBuffer(),
|
|
747
|
+
msgSender.toString(),
|
|
748
|
+
nextL1SlotTs,
|
|
749
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
750
|
+
)
|
|
671
751
|
.catch(err => {
|
|
672
|
-
if (err instanceof FormattedViemError &&
|
|
673
|
-
this.log.warn(`Failed canProposeAtTime check with ${
|
|
752
|
+
if (err instanceof FormattedViemError && expectedErrors.find(e => err.message.includes(e))) {
|
|
753
|
+
this.log.warn(`Failed canProposeAtTime check with ${expectedErrors.find(e => err.message.includes(e))}`, {
|
|
674
754
|
error: err.message,
|
|
675
755
|
});
|
|
676
756
|
} else {
|
|
@@ -681,21 +761,21 @@ export class SequencerPublisher {
|
|
|
681
761
|
}
|
|
682
762
|
|
|
683
763
|
/**
|
|
684
|
-
* @notice Will simulate `
|
|
685
|
-
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header
|
|
686
|
-
* It will throw if the
|
|
687
|
-
* @param header - The
|
|
764
|
+
* @notice Will simulate the rollup's `validateHeaderWithAttestations` to make sure the checkpoint header is valid
|
|
765
|
+
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header,
|
|
766
|
+
* skipping the DA and signature checks. It will throw if the checkpoint header is invalid.
|
|
767
|
+
* @param header - The checkpoint header to validate
|
|
688
768
|
*/
|
|
689
|
-
@trackSpan('SequencerPublisher.
|
|
690
|
-
public async
|
|
769
|
+
@trackSpan('SequencerPublisher.validateCheckpointHeader')
|
|
770
|
+
public async validateCheckpointHeader(
|
|
691
771
|
header: CheckpointHeader,
|
|
692
|
-
|
|
772
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
693
773
|
): Promise<void> {
|
|
694
774
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
695
775
|
|
|
696
776
|
const args = [
|
|
697
777
|
header.toViem(),
|
|
698
|
-
CommitteeAttestationsAndSigners.
|
|
778
|
+
CommitteeAttestationsAndSigners.packAttestations([]),
|
|
699
779
|
[], // no signers
|
|
700
780
|
Signature.empty().toViemSignature(),
|
|
701
781
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
@@ -703,21 +783,13 @@ export class SequencerPublisher {
|
|
|
703
783
|
flags,
|
|
704
784
|
] as const;
|
|
705
785
|
|
|
706
|
-
const
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
let balance = 0n;
|
|
711
|
-
if (this.config.fishermanMode) {
|
|
712
|
-
// In fisherman mode, we can't know where the proposer is publishing from
|
|
713
|
-
// so we just add sufficient balance to the multicall3 address
|
|
714
|
-
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
715
|
-
} else {
|
|
716
|
-
balance = await this.l1TxUtils.getSenderBalance();
|
|
717
|
-
}
|
|
786
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
787
|
+
const ts = getLastL1SlotTimestampForL2Slot(header.slotNumber, l1Constants);
|
|
788
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
789
|
+
// Balance override for compatibility with providers that apply an upfront funds check to simulated calls.
|
|
718
790
|
stateOverrides.push({
|
|
719
791
|
address: MULTI_CALL_3_ADDRESS,
|
|
720
|
-
balance,
|
|
792
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
721
793
|
});
|
|
722
794
|
|
|
723
795
|
await this.l1TxUtils.simulate(
|
|
@@ -838,9 +910,11 @@ export class SequencerPublisher {
|
|
|
838
910
|
const logData = { ...checkpoint, reason };
|
|
839
911
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
840
912
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
)
|
|
913
|
+
// Use the exact packed tuple posted to L1 verbatim. A repack via `packAttestations` is not a
|
|
914
|
+
// byte-faithful inverse of `fromPacked` (a canonicalized yParity byte or an all-zero signature slot
|
|
915
|
+
// round-trips differently), so it would diverge from the stored `attestationsHash` and revert the
|
|
916
|
+
// invalidation.
|
|
917
|
+
const attestationsAndSigners = validationResult.verbatimAttestations;
|
|
844
918
|
|
|
845
919
|
if (reason === 'invalid-attestation') {
|
|
846
920
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -861,43 +935,11 @@ export class SequencerPublisher {
|
|
|
861
935
|
}
|
|
862
936
|
}
|
|
863
937
|
|
|
864
|
-
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
865
|
-
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
866
|
-
public async validateCheckpointForSubmission(
|
|
867
|
-
checkpoint: Checkpoint,
|
|
868
|
-
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
869
|
-
attestationsAndSignersSignature: Signature,
|
|
870
|
-
options: {
|
|
871
|
-
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
872
|
-
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
873
|
-
},
|
|
874
|
-
): Promise<void> {
|
|
875
|
-
const blobFields = checkpoint.toBlobFields();
|
|
876
|
-
const blobs = await getBlobsPerL1Block(blobFields);
|
|
877
|
-
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
878
|
-
|
|
879
|
-
const args = [
|
|
880
|
-
{
|
|
881
|
-
header: checkpoint.header.toViem(),
|
|
882
|
-
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
883
|
-
oracleInput: {
|
|
884
|
-
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
885
|
-
},
|
|
886
|
-
},
|
|
887
|
-
attestationsAndSigners.getPackedAttestations(),
|
|
888
|
-
attestationsAndSigners.getSigners().map(signer => signer.toString()),
|
|
889
|
-
attestationsAndSignersSignature.toViemSignature(),
|
|
890
|
-
blobInput,
|
|
891
|
-
] as const;
|
|
892
|
-
|
|
893
|
-
await this.simulateProposeTx(args, options);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
938
|
private async enqueueCastSignalHelper(
|
|
897
939
|
slotNumber: SlotNumber,
|
|
898
940
|
signalType: GovernanceSignalAction,
|
|
899
941
|
payload: EthAddress,
|
|
900
|
-
base:
|
|
942
|
+
base: GovernanceProposerContract,
|
|
901
943
|
signerAddress: EthAddress,
|
|
902
944
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
903
945
|
): Promise<boolean> {
|
|
@@ -912,6 +954,19 @@ export class SequencerPublisher {
|
|
|
912
954
|
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
913
955
|
return false;
|
|
914
956
|
}
|
|
957
|
+
|
|
958
|
+
const canonicalRollup = await base.getRollupAddress();
|
|
959
|
+
if (!canonicalRollup.equals(EthAddress.fromString(this.rollupContract.address))) {
|
|
960
|
+
this.log.warn(`Rollup ${this.rollupContract.address} is not canonical, skipping governance signal`, {
|
|
961
|
+
slotNumber,
|
|
962
|
+
signalType,
|
|
963
|
+
canonicalRollup,
|
|
964
|
+
targetRollup: this.rollupContract.address,
|
|
965
|
+
payload: payload.toString(),
|
|
966
|
+
});
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
|
|
915
970
|
const round = await base.computeRound(slotNumber);
|
|
916
971
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
917
972
|
|
|
@@ -923,34 +978,43 @@ export class SequencerPublisher {
|
|
|
923
978
|
return false;
|
|
924
979
|
}
|
|
925
980
|
|
|
926
|
-
if (await
|
|
981
|
+
if (await base.isPayloadEmpty(payload)) {
|
|
927
982
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
928
983
|
return false;
|
|
929
984
|
}
|
|
930
985
|
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
986
|
+
// Classify the payload against the Governance proposal history so we stop signalling once its
|
|
987
|
+
// proposal is live or was already executed, while still re-signalling one whose proposal was
|
|
988
|
+
// merely rejected/dropped/expired.
|
|
989
|
+
let status: PayloadProposalStatus = 'none';
|
|
990
|
+
try {
|
|
991
|
+
status = await base.getPayloadProposalStatus(payload.toString());
|
|
992
|
+
} catch (err) {
|
|
993
|
+
// We deliberately swallow the error and proceed to signal. Failing closed (skipping the
|
|
994
|
+
// signal) on transient RPC errors would let a flaky L1 endpoint silence governance
|
|
995
|
+
// participation entirely; failing open at worst produces a duplicate signal that the
|
|
996
|
+
// contract will simply count alongside others in the round.
|
|
997
|
+
this.log.error(`Failed to check governance proposal status for payload ${payload} (signalling anyway)`, err, {
|
|
998
|
+
slotNumber,
|
|
999
|
+
signalType,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (status === 'live') {
|
|
1004
|
+
this.log.info(`Payload ${payload} has a live governance proposal, stopping signals`, {
|
|
1005
|
+
slotNumber,
|
|
1006
|
+
signalType,
|
|
1007
|
+
payload: payload.toString(),
|
|
1008
|
+
});
|
|
1009
|
+
return false;
|
|
950
1010
|
}
|
|
951
1011
|
|
|
952
|
-
if (this.
|
|
953
|
-
this.log.info(
|
|
1012
|
+
if (status === 'executed' && !this.config.governanceProposerForcePayloadVote) {
|
|
1013
|
+
this.log.info(
|
|
1014
|
+
`Payload ${payload} was executed by governance within lookback, stopping signals ` +
|
|
1015
|
+
`(set GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE to re-signal)`,
|
|
1016
|
+
{ slotNumber, signalType, payload: payload.toString() },
|
|
1017
|
+
);
|
|
954
1018
|
return false;
|
|
955
1019
|
}
|
|
956
1020
|
|
|
@@ -972,45 +1036,19 @@ export class SequencerPublisher {
|
|
|
972
1036
|
lastValidL2Slot: slotNumber,
|
|
973
1037
|
});
|
|
974
1038
|
|
|
975
|
-
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
976
|
-
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
977
|
-
|
|
978
|
-
try {
|
|
979
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
980
|
-
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
981
|
-
} catch (err) {
|
|
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
|
-
});
|
|
999
|
-
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
1039
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
1003
1040
|
this.addRequest({
|
|
1004
|
-
gasConfig: { gasLimit: SequencerPublisher.VOTE_GAS_GUESS },
|
|
1005
1041
|
action,
|
|
1006
1042
|
request,
|
|
1007
1043
|
lastValidL2Slot: slotNumber,
|
|
1008
1044
|
checkSuccess: (_request, result) => {
|
|
1009
1045
|
const success =
|
|
1010
1046
|
result &&
|
|
1011
|
-
result.receipt
|
|
1012
|
-
|
|
1013
|
-
|
|
1047
|
+
extractEventSuccess(result.receipt, {
|
|
1048
|
+
address: base.address.toString(),
|
|
1049
|
+
abi: EmpireBaseAbi,
|
|
1050
|
+
eventName: 'SignalCast',
|
|
1051
|
+
});
|
|
1014
1052
|
|
|
1015
1053
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
1016
1054
|
if (!success) {
|
|
@@ -1032,17 +1070,6 @@ export class SequencerPublisher {
|
|
|
1032
1070
|
return true;
|
|
1033
1071
|
}
|
|
1034
1072
|
|
|
1035
|
-
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
1036
|
-
const key = payload.toString();
|
|
1037
|
-
const cached = this.isPayloadEmptyCache.get(key);
|
|
1038
|
-
if (cached) {
|
|
1039
|
-
return cached;
|
|
1040
|
-
}
|
|
1041
|
-
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
1042
|
-
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
1043
|
-
return isEmpty;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
1073
|
/**
|
|
1047
1074
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
1048
1075
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -1064,6 +1091,47 @@ export class SequencerPublisher {
|
|
|
1064
1091
|
);
|
|
1065
1092
|
}
|
|
1066
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* Enqueues a `prune()` transaction if the rollup is prunable at the given slot's L1 timestamp.
|
|
1096
|
+
* `prune()` is permissionless and idempotent — if the chain is no longer prunable by send time the
|
|
1097
|
+
* bundle simulation usually drops the entry; on a node without `eth_simulateV1` the bundle is sent
|
|
1098
|
+
* as-is and the prune reverts `Rollup__NothingToPrune` inside `aggregate3(allowFailure: true)`
|
|
1099
|
+
* (a failed action, never a whole-tx revert). Used by the failed-sync fallback so a stuck pending
|
|
1100
|
+
* chain (e.g. bad data blocking sync) can be wound back to recover.
|
|
1101
|
+
* @returns true if a prune request was enqueued, false otherwise.
|
|
1102
|
+
*/
|
|
1103
|
+
public async enqueuePruneIfPrunable(slotNumber: SlotNumber): Promise<boolean> {
|
|
1104
|
+
if (this.lastActions['prune'] === slotNumber) {
|
|
1105
|
+
this.log.debug(`Skipping duplicate prune for slot ${slotNumber}`, { slotNumber });
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
// Use the SAME timestamp the bundle simulator overrides block.timestamp with at send time
|
|
1109
|
+
// (sequencer-bundle-simulator.ts) so this upfront check and the send-time sim agree. Slot-start
|
|
1110
|
+
// and last-L1-slot both fall within the same L2 slot (and epoch, which is what `canPruneAtTime`
|
|
1111
|
+
// derives), so they agree today; matching the simulator keeps it robust if the contract ever uses
|
|
1112
|
+
// the timestamp more granularly.
|
|
1113
|
+
const ts = getLastL1SlotTimestampForL2Slot(slotNumber, this.epochCache.getL1Constants());
|
|
1114
|
+
const canPrune = await this.rollupContract.canPruneAtTime(ts).catch(err => {
|
|
1115
|
+
this.log.error(`Failed to check canPruneAtTime for slot ${slotNumber}`, err, { slotNumber });
|
|
1116
|
+
return false;
|
|
1117
|
+
});
|
|
1118
|
+
if (!canPrune) {
|
|
1119
|
+
this.log.debug(`Rollup not prunable at slot ${slotNumber}`, { slotNumber });
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
const request: L1TxRequest = {
|
|
1123
|
+
to: this.rollupContract.address,
|
|
1124
|
+
data: encodeFunctionData({ abi: RollupAbi, functionName: 'prune', args: [] }),
|
|
1125
|
+
};
|
|
1126
|
+
this.log.info(`Enqueuing rollup prune for slot ${slotNumber}`, { slotNumber });
|
|
1127
|
+
return this.enqueueRequest(
|
|
1128
|
+
'prune',
|
|
1129
|
+
request,
|
|
1130
|
+
{ address: this.rollupContract.address, abi: RollupAbi, eventName: 'PrunedPending' },
|
|
1131
|
+
slotNumber,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1067
1135
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
1068
1136
|
public async enqueueSlashingActions(
|
|
1069
1137
|
actions: ProposerSlashAction[],
|
|
@@ -1078,55 +1146,6 @@ export class SequencerPublisher {
|
|
|
1078
1146
|
|
|
1079
1147
|
for (const action of actions) {
|
|
1080
1148
|
switch (action.type) {
|
|
1081
|
-
case 'vote-empire-payload': {
|
|
1082
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1083
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
1084
|
-
break;
|
|
1085
|
-
}
|
|
1086
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
1087
|
-
signerAddress,
|
|
1088
|
-
});
|
|
1089
|
-
await this.enqueueCastSignalHelper(
|
|
1090
|
-
slotNumber,
|
|
1091
|
-
'empire-slashing-signal',
|
|
1092
|
-
action.payload,
|
|
1093
|
-
this.slashingProposerContract,
|
|
1094
|
-
signerAddress,
|
|
1095
|
-
signer,
|
|
1096
|
-
);
|
|
1097
|
-
break;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
case 'create-empire-payload': {
|
|
1101
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
1102
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
1103
|
-
await this.simulateAndEnqueueRequest(
|
|
1104
|
-
'create-empire-payload',
|
|
1105
|
-
request,
|
|
1106
|
-
(receipt: TransactionReceipt) =>
|
|
1107
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
1108
|
-
slotNumber,
|
|
1109
|
-
);
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
case 'execute-empire-payload': {
|
|
1114
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
1115
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1116
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
1117
|
-
return false;
|
|
1118
|
-
}
|
|
1119
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
1120
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1121
|
-
await this.simulateAndEnqueueRequest(
|
|
1122
|
-
'execute-empire-payload',
|
|
1123
|
-
request,
|
|
1124
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
1125
|
-
slotNumber,
|
|
1126
|
-
);
|
|
1127
|
-
break;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
1149
|
case 'vote-offenses': {
|
|
1131
1150
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
1132
1151
|
slotNumber,
|
|
@@ -1134,17 +1153,20 @@ export class SequencerPublisher {
|
|
|
1134
1153
|
votesCount: action.votes.length,
|
|
1135
1154
|
signerAddress,
|
|
1136
1155
|
});
|
|
1137
|
-
if (this.slashingProposerContract
|
|
1138
|
-
this.log.error('
|
|
1156
|
+
if (!this.slashingProposerContract) {
|
|
1157
|
+
this.log.error('No slashing proposer contract available');
|
|
1139
1158
|
return false;
|
|
1140
1159
|
}
|
|
1141
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
1142
1160
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1143
|
-
const request = await
|
|
1144
|
-
|
|
1161
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1162
|
+
this.enqueueRequest(
|
|
1145
1163
|
'vote-offenses',
|
|
1146
1164
|
request,
|
|
1147
|
-
|
|
1165
|
+
{
|
|
1166
|
+
address: this.slashingProposerContract.address.toString(),
|
|
1167
|
+
abi: SlashingProposerAbi,
|
|
1168
|
+
eventName: 'VoteCast',
|
|
1169
|
+
},
|
|
1148
1170
|
slotNumber,
|
|
1149
1171
|
);
|
|
1150
1172
|
break;
|
|
@@ -1156,16 +1178,22 @@ export class SequencerPublisher {
|
|
|
1156
1178
|
round: action.round,
|
|
1157
1179
|
signerAddress,
|
|
1158
1180
|
});
|
|
1159
|
-
if (this.slashingProposerContract
|
|
1160
|
-
this.log.error('
|
|
1181
|
+
if (!this.slashingProposerContract) {
|
|
1182
|
+
this.log.error('No slashing proposer contract available');
|
|
1161
1183
|
return false;
|
|
1162
1184
|
}
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1185
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1186
|
+
action.round,
|
|
1187
|
+
action.committees,
|
|
1188
|
+
);
|
|
1189
|
+
this.enqueueRequest(
|
|
1166
1190
|
'execute-slash',
|
|
1167
|
-
|
|
1168
|
-
|
|
1191
|
+
executeRequest,
|
|
1192
|
+
{
|
|
1193
|
+
address: this.slashingProposerContract.address.toString(),
|
|
1194
|
+
abi: SlashingProposerAbi,
|
|
1195
|
+
eventName: 'RoundExecuted',
|
|
1196
|
+
},
|
|
1169
1197
|
slotNumber,
|
|
1170
1198
|
);
|
|
1171
1199
|
break;
|
|
@@ -1181,16 +1209,12 @@ export class SequencerPublisher {
|
|
|
1181
1209
|
return true;
|
|
1182
1210
|
}
|
|
1183
1211
|
|
|
1184
|
-
/**
|
|
1212
|
+
/** Enqueues a proposal for a checkpoint on L1 */
|
|
1185
1213
|
public async enqueueProposeCheckpoint(
|
|
1186
1214
|
checkpoint: Checkpoint,
|
|
1187
1215
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
1188
1216
|
attestationsAndSignersSignature: Signature,
|
|
1189
|
-
opts: {
|
|
1190
|
-
txTimeoutAt?: Date;
|
|
1191
|
-
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1192
|
-
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1193
|
-
} = {},
|
|
1217
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1194
1218
|
): Promise<void> {
|
|
1195
1219
|
const checkpointHeader = checkpoint.header;
|
|
1196
1220
|
|
|
@@ -1206,29 +1230,11 @@ export class SequencerPublisher {
|
|
|
1206
1230
|
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
1207
1231
|
};
|
|
1208
1232
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1215
|
-
await this.validateCheckpointForSubmission(
|
|
1216
|
-
checkpoint,
|
|
1217
|
-
attestationsAndSigners,
|
|
1218
|
-
attestationsAndSignersSignature,
|
|
1219
|
-
opts,
|
|
1220
|
-
);
|
|
1221
|
-
} catch (err: any) {
|
|
1222
|
-
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1223
|
-
...checkpoint.getStats(),
|
|
1224
|
-
slotNumber: checkpoint.header.slotNumber,
|
|
1225
|
-
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
1226
|
-
});
|
|
1227
|
-
throw err;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1231
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts);
|
|
1233
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1234
|
+
...checkpoint.toCheckpointInfo(),
|
|
1235
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1236
|
+
});
|
|
1237
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, { txTimeoutAt: opts.txTimeoutAt });
|
|
1232
1238
|
}
|
|
1233
1239
|
|
|
1234
1240
|
public enqueueInvalidateCheckpoint(
|
|
@@ -1239,23 +1245,22 @@ export class SequencerPublisher {
|
|
|
1239
1245
|
return;
|
|
1240
1246
|
}
|
|
1241
1247
|
|
|
1242
|
-
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1243
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
1244
|
-
|
|
1245
1248
|
const { gasUsed, checkpointNumber } = request;
|
|
1246
|
-
const logData = { gasUsed, checkpointNumber,
|
|
1249
|
+
const logData = { gasUsed, checkpointNumber, opts };
|
|
1247
1250
|
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
1248
1251
|
this.addRequest({
|
|
1249
1252
|
action: `invalidate-by-${request.reason}`,
|
|
1250
1253
|
request: request.request,
|
|
1251
|
-
gasConfig: {
|
|
1254
|
+
gasConfig: opts.txTimeoutAt ? { txTimeoutAt: opts.txTimeoutAt } : undefined,
|
|
1252
1255
|
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
1253
1256
|
checkSuccess: (_req, result) => {
|
|
1254
1257
|
const success =
|
|
1255
1258
|
result &&
|
|
1256
|
-
result.receipt
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
+
extractEventSuccess(result.receipt, {
|
|
1260
|
+
address: this.rollupContract.address,
|
|
1261
|
+
abi: RollupAbi,
|
|
1262
|
+
eventName: 'CheckpointInvalidated',
|
|
1263
|
+
});
|
|
1259
1264
|
if (!success) {
|
|
1260
1265
|
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
1261
1266
|
} else {
|
|
@@ -1266,73 +1271,36 @@ export class SequencerPublisher {
|
|
|
1266
1271
|
});
|
|
1267
1272
|
}
|
|
1268
1273
|
|
|
1269
|
-
|
|
1274
|
+
/**
|
|
1275
|
+
* Dedup-checked enqueue helper for actions that are simulated at bundle-send time rather
|
|
1276
|
+
* than at enqueue time. Validates the (action, slot) dedup key, sets `lastActions`, and
|
|
1277
|
+
* enqueues without a gasLimit so the bundle simulate sets the only gasLimit that matters.
|
|
1278
|
+
*/
|
|
1279
|
+
private enqueueRequest(
|
|
1270
1280
|
action: Action,
|
|
1271
1281
|
request: L1TxRequest,
|
|
1272
|
-
|
|
1282
|
+
eventOpts: { address: string; abi: Abi; eventName: string },
|
|
1273
1283
|
slotNumber: SlotNumber,
|
|
1274
|
-
) {
|
|
1275
|
-
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1276
|
-
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
1284
|
+
): boolean {
|
|
1277
1285
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
1278
1286
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
1279
1287
|
return false;
|
|
1280
1288
|
}
|
|
1281
|
-
|
|
1282
1289
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1283
1290
|
this.lastActions[action] = slotNumber;
|
|
1284
1291
|
|
|
1285
|
-
this.log.debug(`
|
|
1286
|
-
|
|
1287
|
-
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1288
|
-
|
|
1289
|
-
let gasUsed: bigint;
|
|
1290
|
-
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1291
|
-
|
|
1292
|
-
try {
|
|
1293
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1294
|
-
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1295
|
-
} catch (err) {
|
|
1296
|
-
const viemError = formatViemError(err, simulateAbi);
|
|
1297
|
-
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
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
|
-
|
|
1312
|
-
return false;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1316
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1317
|
-
logData.gasLimit = gasLimit;
|
|
1318
|
-
|
|
1319
|
-
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1320
|
-
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1321
|
-
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1322
|
-
|
|
1323
|
-
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1292
|
+
this.log.debug(`Enqueuing ${action}`, { slotNumber });
|
|
1324
1293
|
this.addRequest({
|
|
1325
1294
|
action,
|
|
1326
|
-
request
|
|
1327
|
-
gasConfig: { gasLimit },
|
|
1295
|
+
request,
|
|
1328
1296
|
lastValidL2Slot: slotNumber,
|
|
1329
|
-
checkSuccess: (
|
|
1330
|
-
const success = result && result.receipt
|
|
1297
|
+
checkSuccess: (_request, result) => {
|
|
1298
|
+
const success = result && extractEventSuccess(result.receipt, eventOpts);
|
|
1331
1299
|
if (!success) {
|
|
1332
|
-
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result,
|
|
1300
|
+
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result, slotNumber });
|
|
1333
1301
|
this.lastActions[action] = cachedLastActionSlot;
|
|
1334
1302
|
} else {
|
|
1335
|
-
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result,
|
|
1303
|
+
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result, slotNumber });
|
|
1336
1304
|
}
|
|
1337
1305
|
return !!success;
|
|
1338
1306
|
},
|
|
@@ -1358,10 +1326,7 @@ export class SequencerPublisher {
|
|
|
1358
1326
|
this.l1TxUtils.restart();
|
|
1359
1327
|
}
|
|
1360
1328
|
|
|
1361
|
-
private async prepareProposeTx(
|
|
1362
|
-
encodedData: L1ProcessArgs,
|
|
1363
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1364
|
-
) {
|
|
1329
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs) {
|
|
1365
1330
|
const kzg = Blob.getViemKzgInstance();
|
|
1366
1331
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1367
1332
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1374,7 +1339,11 @@ export class SequencerPublisher {
|
|
|
1374
1339
|
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
1375
1340
|
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
1376
1341
|
} else {
|
|
1377
|
-
//
|
|
1342
|
+
// We call validateBlobs via estimateGas with real blob+kzg sidecars as a consistency check
|
|
1343
|
+
// that our locally-built blob commitments match the blob data. The bundle simulate at send
|
|
1344
|
+
// time uses eth_simulateV1, which cannot carry blob inputs, so the rollup's on-chain blob
|
|
1345
|
+
// check is forced off there — making this the only pre-flight detector of a commitment/data
|
|
1346
|
+
// mismatch. The returned gas estimate is stashed on the request for the bundle path to read.
|
|
1378
1347
|
blobEvaluationGas = await this.l1TxUtils
|
|
1379
1348
|
.estimateGas(
|
|
1380
1349
|
this.getSenderAddress().toString(),
|
|
@@ -1432,149 +1401,21 @@ export class SequencerPublisher {
|
|
|
1432
1401
|
blobInput,
|
|
1433
1402
|
] as const;
|
|
1434
1403
|
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
/**
|
|
1441
|
-
* Simulates the propose tx with eth_simulateV1
|
|
1442
|
-
* @param args - The propose tx args
|
|
1443
|
-
* @returns The simulation result
|
|
1444
|
-
*/
|
|
1445
|
-
private async simulateProposeTx(
|
|
1446
|
-
args: readonly [
|
|
1447
|
-
{
|
|
1448
|
-
readonly header: ViemHeader;
|
|
1449
|
-
readonly archive: `0x${string}`;
|
|
1450
|
-
readonly oracleInput: {
|
|
1451
|
-
readonly feeAssetPriceModifier: bigint;
|
|
1452
|
-
};
|
|
1453
|
-
},
|
|
1454
|
-
ViemCommitteeAttestations,
|
|
1455
|
-
`0x${string}`[], // Signers
|
|
1456
|
-
ViemSignature,
|
|
1457
|
-
`0x${string}`,
|
|
1458
|
-
],
|
|
1459
|
-
options: {
|
|
1460
|
-
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1461
|
-
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1462
|
-
},
|
|
1463
|
-
) {
|
|
1464
|
-
const rollupData = encodeFunctionData({
|
|
1465
|
-
abi: RollupAbi,
|
|
1466
|
-
functionName: 'propose',
|
|
1467
|
-
args,
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
// override the proposed checkpoint number if requested
|
|
1471
|
-
const forcePendingCheckpointNumberStateDiff = (
|
|
1472
|
-
options.forcePendingCheckpointNumber !== undefined
|
|
1473
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1474
|
-
: []
|
|
1475
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
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
|
-
|
|
1487
|
-
const stateOverrides: StateOverride = [
|
|
1488
|
-
{
|
|
1489
|
-
address: this.rollupContract.address,
|
|
1490
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1491
|
-
stateDiff: [
|
|
1492
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1493
|
-
...forcePendingCheckpointNumberStateDiff,
|
|
1494
|
-
...forceProposedFeeHeaderStateDiff,
|
|
1495
|
-
],
|
|
1496
|
-
},
|
|
1497
|
-
];
|
|
1498
|
-
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1499
|
-
if (this.proposerAddressForSimulation) {
|
|
1500
|
-
stateOverrides.push({
|
|
1501
|
-
address: this.proposerAddressForSimulation.toString(),
|
|
1502
|
-
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1503
|
-
});
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1507
|
-
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1508
|
-
|
|
1509
|
-
const simulationResult = await this.l1TxUtils
|
|
1510
|
-
.simulate(
|
|
1511
|
-
{
|
|
1512
|
-
to: this.rollupContract.address,
|
|
1513
|
-
data: rollupData,
|
|
1514
|
-
gas: MAX_L1_TX_LIMIT,
|
|
1515
|
-
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
time: simTs,
|
|
1519
|
-
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1520
|
-
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1521
|
-
},
|
|
1522
|
-
stateOverrides,
|
|
1523
|
-
RollupAbi,
|
|
1524
|
-
{
|
|
1525
|
-
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1526
|
-
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1527
|
-
},
|
|
1528
|
-
)
|
|
1529
|
-
.catch(err => {
|
|
1530
|
-
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1531
|
-
const viemError = formatViemError(err);
|
|
1532
|
-
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1533
|
-
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1534
|
-
// Return a minimal simulation result with the fallback gas estimate
|
|
1535
|
-
return {
|
|
1536
|
-
gasUsed: MAX_L1_TX_LIMIT,
|
|
1537
|
-
logs: [],
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
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
|
-
});
|
|
1553
|
-
throw err;
|
|
1554
|
-
});
|
|
1404
|
+
const rollupData = encodeFunctionData({ abi: RollupAbi, functionName: 'propose', args });
|
|
1555
1405
|
|
|
1556
|
-
return {
|
|
1406
|
+
return { args, blobEvaluationGas, rollupData };
|
|
1557
1407
|
}
|
|
1558
1408
|
|
|
1559
1409
|
private async addProposeTx(
|
|
1560
1410
|
checkpoint: Checkpoint,
|
|
1561
1411
|
encodedData: L1ProcessArgs,
|
|
1562
|
-
opts: {
|
|
1563
|
-
txTimeoutAt?: Date;
|
|
1564
|
-
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
1565
|
-
forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
1566
|
-
} = {},
|
|
1412
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1567
1413
|
): Promise<void> {
|
|
1568
1414
|
const slot = checkpoint.header.slotNumber;
|
|
1569
1415
|
const timer = new Timer();
|
|
1570
1416
|
const kzg = Blob.getViemKzgInstance();
|
|
1571
|
-
const { rollupData,
|
|
1417
|
+
const { rollupData, blobEvaluationGas } = await this.prepareProposeTx(encodedData);
|
|
1572
1418
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1573
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
1574
|
-
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
1575
|
-
blobEvaluationGas +
|
|
1576
|
-
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1577
|
-
);
|
|
1578
1419
|
|
|
1579
1420
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1580
1421
|
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
@@ -1591,7 +1432,8 @@ export class SequencerPublisher {
|
|
|
1591
1432
|
data: rollupData,
|
|
1592
1433
|
},
|
|
1593
1434
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1594
|
-
gasConfig: {
|
|
1435
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit: undefined },
|
|
1436
|
+
blobEvaluationGas,
|
|
1595
1437
|
blobConfig: {
|
|
1596
1438
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1597
1439
|
kzg,
|
|
@@ -1601,10 +1443,11 @@ export class SequencerPublisher {
|
|
|
1601
1443
|
return false;
|
|
1602
1444
|
}
|
|
1603
1445
|
const { receipt, stats, errorMsg } = result;
|
|
1604
|
-
const success =
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1446
|
+
const success = extractEventSuccess(receipt, {
|
|
1447
|
+
address: this.rollupContract.address,
|
|
1448
|
+
abi: RollupAbi,
|
|
1449
|
+
eventName: 'CheckpointProposed',
|
|
1450
|
+
});
|
|
1608
1451
|
|
|
1609
1452
|
if (success) {
|
|
1610
1453
|
const endBlock = receipt.blockNumber;
|
|
@@ -1645,13 +1488,6 @@ export class SequencerPublisher {
|
|
|
1645
1488
|
});
|
|
1646
1489
|
}
|
|
1647
1490
|
|
|
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
1491
|
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1656
1492
|
private getNextL1SlotTimestamp(): bigint {
|
|
1657
1493
|
const l1Constants = this.epochCache.getL1Constants();
|