@aztec/sequencer-client 0.0.1-commit.f504929 → 0.0.1-commit.f5a9928
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 +23 -5
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +63 -36
- package/dest/config.d.ts +32 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +81 -31
- 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 +13 -26
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +9 -66
- package/dest/global_variable_builder/index.d.ts +4 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/publisher/config.d.ts +19 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +30 -5
- 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 +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +2 -3
- package/dest/publisher/sequencer-publisher.d.ts +91 -69
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +445 -535
- 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 +75 -19
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +831 -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/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/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 +62 -5
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +14 -11
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +45 -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 +166 -39
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +616 -198
- 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/mock_checkpoint_builder.d.ts +7 -9
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +39 -30
- package/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 -28
- package/src/client/sequencer-client.ts +90 -45
- package/src/config.ts +98 -31
- 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 +19 -88
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +62 -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 +3 -6
- package/src/publisher/sequencer-publisher.ts +510 -584
- 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 +993 -294
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/errors.ts +0 -15
- package/src/sequencer/events.ts +66 -5
- package/src/sequencer/metrics.ts +63 -25
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +720 -220
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -2
- package/src/test/mock_checkpoint_builder.ts +51 -48
- package/src/test/utils.ts +61 -10
- package/dest/sequencer/timetable.d.ts +0 -87
- package/dest/sequencer/timetable.d.ts.map +0 -1
- package/dest/sequencer/timetable.js +0 -223
- package/src/sequencer/README.md +0 -531
- package/src/sequencer/timetable.ts +0 -283
|
@@ -3,16 +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
7
|
type GovernanceProposerContract,
|
|
9
|
-
type IEmpireBase,
|
|
10
8
|
MULTI_CALL_3_ADDRESS,
|
|
11
9
|
Multicall3,
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
type
|
|
10
|
+
MulticallForwarderRevertedError,
|
|
11
|
+
type PayloadProposalStatus,
|
|
12
|
+
type RollupContract,
|
|
13
|
+
type SimulationOverridesPlan,
|
|
14
|
+
type SlashingProposerContract,
|
|
15
|
+
buildSimulationOverridesStateOverride,
|
|
16
16
|
} from '@aztec/ethereum/contracts';
|
|
17
17
|
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
18
18
|
import {
|
|
@@ -24,43 +24,82 @@ import {
|
|
|
24
24
|
type TransactionStats,
|
|
25
25
|
WEI_CONST,
|
|
26
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
import {
|
|
28
|
+
FormattedViemError,
|
|
29
|
+
formatViemError,
|
|
30
|
+
mergeAbis,
|
|
31
|
+
tryDecodeRevertReason,
|
|
32
|
+
tryExtractEvent,
|
|
33
|
+
} from '@aztec/ethereum/utils';
|
|
30
34
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
35
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
31
36
|
import { pick } from '@aztec/foundation/collection';
|
|
32
37
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
33
38
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
34
39
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
35
|
-
import { Signature
|
|
40
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
36
41
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
37
|
-
import {
|
|
42
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
38
43
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
39
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
40
|
-
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
44
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
45
|
+
import { EmpireBaseAbi, ErrorsAbi, RollupAbi, SlashingProposerAbi } from '@aztec/l1-artifacts';
|
|
41
46
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
42
47
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
43
48
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
44
|
-
import {
|
|
49
|
+
import type { SequencerConfig } from '@aztec/stdlib/config';
|
|
50
|
+
import {
|
|
51
|
+
getLastL1SlotTimestampForL2Slot,
|
|
52
|
+
getNextL1SlotTimestamp,
|
|
53
|
+
getTimestampForSlot,
|
|
54
|
+
} from '@aztec/stdlib/epoch-helpers';
|
|
45
55
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
46
56
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
47
57
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
48
58
|
|
|
49
59
|
import {
|
|
60
|
+
type Abi,
|
|
50
61
|
type Hex,
|
|
51
|
-
type StateOverride,
|
|
52
62
|
type TransactionReceipt,
|
|
53
63
|
type TypedDataDefinition,
|
|
54
64
|
encodeFunctionData,
|
|
55
65
|
keccak256,
|
|
56
|
-
multicall3Abi,
|
|
57
66
|
toHex,
|
|
58
67
|
} from 'viem';
|
|
59
68
|
|
|
60
69
|
import type { SequencerPublisherConfig } from './config.js';
|
|
61
70
|
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
71
|
+
import { type DroppedRequest, SequencerBundleSimulator } from './sequencer-bundle-simulator.js';
|
|
62
72
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
63
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
|
+
|
|
89
|
+
/** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
|
|
90
|
+
export type SendRequestsResult = {
|
|
91
|
+
/** The L1 transaction receipt from the bundled multicall. */
|
|
92
|
+
result: { receipt: TransactionReceipt };
|
|
93
|
+
/** Actions that expired (past their deadline) before the request was sent. */
|
|
94
|
+
expiredActions: Action[];
|
|
95
|
+
/** Actions that were included in the sent L1 transaction. */
|
|
96
|
+
sentActions: Action[];
|
|
97
|
+
/** Actions whose L1 simulation succeeded (subset of sentActions). */
|
|
98
|
+
successfulActions: Action[];
|
|
99
|
+
/** Actions whose L1 simulation failed (subset of sentActions). */
|
|
100
|
+
failedActions: Action[];
|
|
101
|
+
};
|
|
102
|
+
|
|
64
103
|
/** Arguments to the process method of the rollup contract */
|
|
65
104
|
type L1ProcessArgs = {
|
|
66
105
|
/** The L2 block header. */
|
|
@@ -80,18 +119,16 @@ type L1ProcessArgs = {
|
|
|
80
119
|
export const Actions = [
|
|
81
120
|
'invalidate-by-invalid-attestation',
|
|
82
121
|
'invalidate-by-insufficient-attestations',
|
|
122
|
+
'prune',
|
|
83
123
|
'propose',
|
|
84
124
|
'governance-signal',
|
|
85
|
-
'empire-slashing-signal',
|
|
86
|
-
'create-empire-payload',
|
|
87
|
-
'execute-empire-payload',
|
|
88
125
|
'vote-offenses',
|
|
89
126
|
'execute-slash',
|
|
90
127
|
] as const;
|
|
91
128
|
|
|
92
129
|
export type Action = (typeof Actions)[number];
|
|
93
130
|
|
|
94
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
131
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
95
132
|
|
|
96
133
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
97
134
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -102,14 +139,22 @@ export type InvalidateCheckpointRequest = {
|
|
|
102
139
|
gasUsed: bigint;
|
|
103
140
|
checkpointNumber: CheckpointNumber;
|
|
104
141
|
forcePendingCheckpointNumber: CheckpointNumber;
|
|
142
|
+
/** Archive at the rollback target checkpoint (checkpoint N-1). */
|
|
143
|
+
lastArchive: Fr;
|
|
105
144
|
};
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
type EnqueueProposeCheckpointOpts = {
|
|
147
|
+
txTimeoutAt?: Date;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export interface RequestWithExpiry {
|
|
108
151
|
action: Action;
|
|
109
152
|
request: L1TxRequest;
|
|
110
153
|
lastValidL2Slot: SlotNumber;
|
|
111
154
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
112
155
|
blobConfig?: L1BlobInputs;
|
|
156
|
+
/** Gas consumed by validateBlobs; stashed for the bundle simulate at send time. */
|
|
157
|
+
blobEvaluationGas?: bigint;
|
|
113
158
|
checkSuccess: (
|
|
114
159
|
request: L1TxRequest,
|
|
115
160
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -119,24 +164,29 @@ interface RequestWithExpiry {
|
|
|
119
164
|
export class SequencerPublisher {
|
|
120
165
|
private interrupted = false;
|
|
121
166
|
private metrics: SequencerPublisherMetrics;
|
|
167
|
+
private bundleSimulator: SequencerBundleSimulator;
|
|
122
168
|
public epochCache: EpochCache;
|
|
123
169
|
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
|
|
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]);
|
|
127
177
|
|
|
128
178
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
129
179
|
|
|
130
|
-
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
131
|
-
private payloadProposedCache: Set<string> = new Set<string>();
|
|
132
|
-
|
|
133
180
|
protected log: Logger;
|
|
134
181
|
protected ethereumSlotDuration: bigint;
|
|
182
|
+
protected aztecSlotDuration: bigint;
|
|
183
|
+
private readonly previousL1BlockWaitTimeoutMs: number;
|
|
184
|
+
private readonly previousL1BlockWaitPollIntervalMs: number;
|
|
135
185
|
|
|
136
|
-
|
|
186
|
+
/** Date provider for wall-clock time. */
|
|
187
|
+
private readonly dateProvider: DateProvider;
|
|
137
188
|
|
|
138
|
-
|
|
139
|
-
private proposerAddressForSimulation?: EthAddress;
|
|
189
|
+
private blobClient: BlobClientInterface;
|
|
140
190
|
|
|
141
191
|
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
142
192
|
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
@@ -147,33 +197,35 @@ export class SequencerPublisher {
|
|
|
147
197
|
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
148
198
|
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
149
199
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
154
|
-
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
200
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
201
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
155
202
|
|
|
156
203
|
public l1TxUtils: L1TxUtils;
|
|
157
204
|
public rollupContract: RollupContract;
|
|
158
205
|
public govProposerContract: GovernanceProposerContract;
|
|
159
|
-
public slashingProposerContract:
|
|
160
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
206
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
161
207
|
|
|
162
208
|
public readonly tracer: Tracer;
|
|
163
209
|
|
|
164
210
|
protected requests: RequestWithExpiry[] = [];
|
|
165
211
|
|
|
166
212
|
constructor(
|
|
167
|
-
private config: Pick<
|
|
168
|
-
|
|
213
|
+
private config: Pick<
|
|
214
|
+
SequencerPublisherConfig,
|
|
215
|
+
| 'fishermanMode'
|
|
216
|
+
| 'l1TxFailedStore'
|
|
217
|
+
| 'sequencerPublisherPreviousL1BlockWaitTimeoutMs'
|
|
218
|
+
| 'sequencerPublisherPreviousL1BlockWaitPollIntervalMs'
|
|
219
|
+
> &
|
|
220
|
+
Pick<SequencerConfig, 'governanceProposerForcePayloadVote'> &
|
|
221
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
169
222
|
deps: {
|
|
170
223
|
telemetry?: TelemetryClient;
|
|
171
224
|
blobClient: BlobClientInterface;
|
|
172
225
|
l1TxUtils: L1TxUtils;
|
|
173
226
|
rollupContract: RollupContract;
|
|
174
|
-
slashingProposerContract:
|
|
227
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
175
228
|
governanceProposerContract: GovernanceProposerContract;
|
|
176
|
-
slashFactoryContract: SlashFactoryContract;
|
|
177
229
|
epochCache: EpochCache;
|
|
178
230
|
dateProvider: DateProvider;
|
|
179
231
|
metrics: SequencerPublisherMetrics;
|
|
@@ -184,10 +236,15 @@ export class SequencerPublisher {
|
|
|
184
236
|
) {
|
|
185
237
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
186
238
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
239
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
240
|
+
this.previousL1BlockWaitTimeoutMs = config.sequencerPublisherPreviousL1BlockWaitTimeoutMs;
|
|
241
|
+
this.previousL1BlockWaitPollIntervalMs = config.sequencerPublisherPreviousL1BlockWaitPollIntervalMs;
|
|
242
|
+
this.dateProvider = deps.dateProvider;
|
|
187
243
|
this.epochCache = deps.epochCache;
|
|
188
244
|
this.lastActions = deps.lastActions;
|
|
189
245
|
|
|
190
246
|
this.blobClient = deps.blobClient;
|
|
247
|
+
this.dateProvider = deps.dateProvider;
|
|
191
248
|
|
|
192
249
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
193
250
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
@@ -205,14 +262,12 @@ export class SequencerPublisher {
|
|
|
205
262
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
206
263
|
this.slashingProposerContract = newSlashingProposer;
|
|
207
264
|
});
|
|
208
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
209
|
-
|
|
210
265
|
// Initialize L1 fee analyzer for fisherman mode
|
|
211
266
|
if (config.fishermanMode) {
|
|
212
267
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
213
268
|
this.l1TxUtils.client,
|
|
214
269
|
deps.dateProvider,
|
|
215
|
-
|
|
270
|
+
this.log.createChild('fee-analyzer'),
|
|
216
271
|
);
|
|
217
272
|
}
|
|
218
273
|
|
|
@@ -220,11 +275,18 @@ export class SequencerPublisher {
|
|
|
220
275
|
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
221
276
|
this.l1TxUtils.client,
|
|
222
277
|
this.rollupContract,
|
|
223
|
-
|
|
278
|
+
this.log.createChild('price-oracle'),
|
|
224
279
|
);
|
|
225
280
|
|
|
226
281
|
// Initialize failed L1 tx store (optional, for test networks)
|
|
227
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
|
+
});
|
|
228
290
|
}
|
|
229
291
|
|
|
230
292
|
/**
|
|
@@ -255,10 +317,14 @@ export class SequencerPublisher {
|
|
|
255
317
|
|
|
256
318
|
/**
|
|
257
319
|
* Gets the fee asset price modifier from the oracle.
|
|
258
|
-
*
|
|
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.
|
|
259
325
|
*/
|
|
260
|
-
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
261
|
-
return this.feeAssetPriceOracle.computePriceModifier();
|
|
326
|
+
public getFeeAssetPriceModifier(predictedParentEthPerFeeAssetE12?: bigint): Promise<bigint> {
|
|
327
|
+
return this.feeAssetPriceOracle.computePriceModifier(predictedParentEthPerFeeAssetE12);
|
|
262
328
|
}
|
|
263
329
|
|
|
264
330
|
public getSenderAddress() {
|
|
@@ -272,20 +338,12 @@ export class SequencerPublisher {
|
|
|
272
338
|
return this.l1FeeAnalyzer;
|
|
273
339
|
}
|
|
274
340
|
|
|
275
|
-
/**
|
|
276
|
-
* Sets the proposer address to use for simulations in fisherman mode.
|
|
277
|
-
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
278
|
-
*/
|
|
279
|
-
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
280
|
-
this.proposerAddressForSimulation = proposerAddress;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
341
|
public addRequest(request: RequestWithExpiry) {
|
|
284
342
|
this.requests.push(request);
|
|
285
343
|
}
|
|
286
344
|
|
|
287
345
|
public getCurrentL2Slot(): SlotNumber {
|
|
288
|
-
return this.epochCache.
|
|
346
|
+
return this.epochCache.getSlotNow();
|
|
289
347
|
}
|
|
290
348
|
|
|
291
349
|
/**
|
|
@@ -357,22 +415,26 @@ export class SequencerPublisher {
|
|
|
357
415
|
|
|
358
416
|
/**
|
|
359
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.
|
|
360
422
|
* @returns one of:
|
|
361
423
|
* - A receipt and stats if the tx succeeded
|
|
362
424
|
* - a receipt and errorMsg if it failed on L1
|
|
363
425
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
364
426
|
*/
|
|
365
427
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
366
|
-
public async sendRequests() {
|
|
428
|
+
public async sendRequests(targetSlot?: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
367
429
|
const requestsToProcess = [...this.requests];
|
|
368
430
|
this.requests = [];
|
|
431
|
+
|
|
369
432
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
370
433
|
return undefined;
|
|
371
434
|
}
|
|
372
|
-
const currentL2Slot = this.getCurrentL2Slot();
|
|
435
|
+
const currentL2Slot = targetSlot ?? this.getCurrentL2Slot();
|
|
373
436
|
this.log.debug(`Sending requests on L2 slot ${currentL2Slot}`);
|
|
374
437
|
const validRequests = requestsToProcess.filter(request => request.lastValidL2Slot >= currentL2Slot);
|
|
375
|
-
const validActions = validRequests.map(x => x.action);
|
|
376
438
|
const expiredActions = requestsToProcess
|
|
377
439
|
.filter(request => request.lastValidL2Slot < currentL2Slot)
|
|
378
440
|
.map(x => x.action);
|
|
@@ -395,70 +457,60 @@ export class SequencerPublisher {
|
|
|
395
457
|
return undefined;
|
|
396
458
|
}
|
|
397
459
|
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
401
|
-
const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
402
|
-
const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
403
|
-
|
|
404
|
-
if (blobConfigs.length > 1) {
|
|
405
|
-
throw new Error('Multiple blob configs found');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const blobConfig = blobConfigs[0];
|
|
409
|
-
|
|
410
|
-
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
411
|
-
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
412
|
-
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
413
|
-
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
414
|
-
const maxGas = MAX_L1_TX_LIMIT;
|
|
415
|
-
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
416
|
-
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
417
|
-
requested: gasLimit,
|
|
418
|
-
capped: maxGas,
|
|
419
|
-
});
|
|
420
|
-
gasLimit = maxGas;
|
|
421
|
-
}
|
|
460
|
+
// Collect earliest txTimeoutAt across all requests.
|
|
461
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
422
462
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
423
|
-
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined;
|
|
424
|
-
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
463
|
+
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined;
|
|
425
464
|
|
|
426
465
|
// Sort the requests so that proposals always go first
|
|
427
466
|
// This ensures the committee gets precomputed correctly
|
|
428
467
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
429
468
|
|
|
430
469
|
try {
|
|
431
|
-
//
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
470
|
+
// Bundle-level eth_simulateV1: filters out entries that revert and derives the gasLimit.
|
|
471
|
+
const bundleResult = await this.bundleSimulator.simulate(validRequests, currentL2Slot);
|
|
472
|
+
|
|
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;
|
|
445
489
|
|
|
446
|
-
|
|
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 };
|
|
447
496
|
|
|
448
497
|
this.log.debug('Forwarding transactions', {
|
|
449
|
-
|
|
498
|
+
requests: requests.map(request => request.action),
|
|
450
499
|
txConfig,
|
|
451
500
|
});
|
|
452
|
-
const result = await this.forwardWithPublisherRotation(
|
|
501
|
+
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
|
|
453
502
|
if (result === undefined) {
|
|
454
503
|
return undefined;
|
|
455
504
|
}
|
|
456
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
457
|
-
|
|
505
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(requests, result);
|
|
506
|
+
const allFailedActions = [...failedActions, ...droppedRequests.map(d => d.request.action)];
|
|
507
|
+
return {
|
|
458
508
|
result,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
509
|
+
expiredActions,
|
|
510
|
+
sentActions: requests.map(x => x.action),
|
|
511
|
+
successfulActions,
|
|
512
|
+
failedActions: allFailedActions,
|
|
513
|
+
};
|
|
462
514
|
} catch (err) {
|
|
463
515
|
const viemError = formatViemError(err);
|
|
464
516
|
this.log.error(`Failed to publish bundled transactions`, viemError);
|
|
@@ -475,6 +527,40 @@ export class SequencerPublisher {
|
|
|
475
527
|
}
|
|
476
528
|
}
|
|
477
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
|
+
|
|
478
564
|
/**
|
|
479
565
|
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
480
566
|
* failure occurs (i.e. the tx never reached the chain).
|
|
@@ -485,19 +571,30 @@ export class SequencerPublisher {
|
|
|
485
571
|
txConfig: RequestWithExpiry['gasConfig'],
|
|
486
572
|
blobConfig: L1BlobInputs | undefined,
|
|
487
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
|
+
|
|
488
579
|
const triedAddresses: EthAddress[] = [];
|
|
489
580
|
let currentPublisher = this.l1TxUtils;
|
|
490
581
|
|
|
491
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
|
+
}
|
|
492
589
|
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
590
|
+
|
|
493
591
|
try {
|
|
494
592
|
const result = await Multicall3.forward(
|
|
495
593
|
validRequests.map(r => r.request),
|
|
496
594
|
currentPublisher,
|
|
497
|
-
|
|
595
|
+
txConfigWithGasLimit,
|
|
498
596
|
blobConfig,
|
|
499
|
-
|
|
500
|
-
this.log,
|
|
597
|
+
{ gasLimitRequired: true },
|
|
501
598
|
);
|
|
502
599
|
this.l1TxUtils = currentPublisher;
|
|
503
600
|
return result;
|
|
@@ -505,6 +602,12 @@ export class SequencerPublisher {
|
|
|
505
602
|
if (err instanceof TimeoutError) {
|
|
506
603
|
throw err;
|
|
507
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
|
+
}
|
|
508
611
|
const viemError = formatViemError(err);
|
|
509
612
|
if (!this.getNextPublisher) {
|
|
510
613
|
this.log.error('Failed to publish bundled transactions', viemError);
|
|
@@ -516,7 +619,11 @@ export class SequencerPublisher {
|
|
|
516
619
|
);
|
|
517
620
|
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
518
621
|
if (!nextPublisher) {
|
|
519
|
-
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
|
+
);
|
|
520
627
|
return undefined;
|
|
521
628
|
}
|
|
522
629
|
currentPublisher = nextPublisher;
|
|
@@ -524,87 +631,126 @@ export class SequencerPublisher {
|
|
|
524
631
|
}
|
|
525
632
|
}
|
|
526
633
|
|
|
634
|
+
/*
|
|
635
|
+
* Schedules sending all enqueued requests at (or after) the start of the given L2 slot.
|
|
636
|
+
*/
|
|
637
|
+
public async sendRequestsAt(targetSlot: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
638
|
+
await this.waitForTargetSlot(targetSlot);
|
|
639
|
+
if (this.interrupted) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
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
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
527
701
|
private callbackBundledTransactions(
|
|
528
702
|
requests: RequestWithExpiry[],
|
|
529
|
-
result: { receipt: TransactionReceipt;
|
|
530
|
-
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
703
|
+
result: { receipt: TransactionReceipt; multicallData: Hex },
|
|
531
704
|
) {
|
|
532
705
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
|
|
551
|
-
const successfulActions: Action[] = [];
|
|
552
|
-
const failedActions: Action[] = [];
|
|
553
|
-
for (const request of requests) {
|
|
554
|
-
if (request.checkSuccess(request.request, result)) {
|
|
555
|
-
successfulActions.push(request.action);
|
|
556
|
-
} else {
|
|
557
|
-
failedActions.push(request.action);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
// Single backup for the whole reverted tx
|
|
561
|
-
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
562
|
-
this.backupFailedTx({
|
|
563
|
-
id: result.receipt.transactionHash,
|
|
564
|
-
failureType: 'revert',
|
|
565
|
-
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
566
|
-
blobData: txContext.blobData,
|
|
567
|
-
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
568
|
-
receipt: {
|
|
569
|
-
transactionHash: result.receipt.transactionHash,
|
|
570
|
-
blockNumber: result.receipt.blockNumber.toString(),
|
|
571
|
-
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
572
|
-
status: 'reverted',
|
|
573
|
-
},
|
|
574
|
-
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
575
|
-
context: {
|
|
576
|
-
actions: failedActions,
|
|
577
|
-
requests: requests
|
|
578
|
-
.filter(r => failedActions.includes(r.action))
|
|
579
|
-
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
580
|
-
sender: this.getSenderAddress().toString(),
|
|
581
|
-
},
|
|
582
|
-
});
|
|
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);
|
|
583
723
|
}
|
|
584
|
-
return { successfulActions, failedActions };
|
|
585
724
|
}
|
|
725
|
+
return { successfulActions, failedActions };
|
|
586
726
|
}
|
|
587
727
|
|
|
588
728
|
/**
|
|
589
|
-
* @notice Will call `
|
|
729
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
590
730
|
* @param tipArchive - The archive to check
|
|
591
731
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
592
732
|
*/
|
|
593
|
-
public
|
|
594
|
-
tipArchive: Fr,
|
|
595
|
-
msgSender: EthAddress,
|
|
596
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
597
|
-
) {
|
|
733
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
598
734
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
599
|
-
|
|
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'];
|
|
740
|
+
|
|
741
|
+
const slotOffset = this.aztecSlotDuration;
|
|
742
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
600
743
|
|
|
601
744
|
return this.rollupContract
|
|
602
|
-
.
|
|
603
|
-
|
|
604
|
-
|
|
745
|
+
.canProposeAt(
|
|
746
|
+
tipArchive.toBuffer(),
|
|
747
|
+
msgSender.toString(),
|
|
748
|
+
nextL1SlotTs,
|
|
749
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
750
|
+
)
|
|
605
751
|
.catch(err => {
|
|
606
|
-
if (err instanceof FormattedViemError &&
|
|
607
|
-
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))}`, {
|
|
608
754
|
error: err.message,
|
|
609
755
|
});
|
|
610
756
|
} else {
|
|
@@ -613,22 +759,23 @@ export class SequencerPublisher {
|
|
|
613
759
|
return undefined;
|
|
614
760
|
});
|
|
615
761
|
}
|
|
762
|
+
|
|
616
763
|
/**
|
|
617
|
-
* @notice Will simulate `
|
|
618
|
-
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header
|
|
619
|
-
* It will throw if the
|
|
620
|
-
* @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
|
|
621
768
|
*/
|
|
622
|
-
@trackSpan('SequencerPublisher.
|
|
623
|
-
public async
|
|
769
|
+
@trackSpan('SequencerPublisher.validateCheckpointHeader')
|
|
770
|
+
public async validateCheckpointHeader(
|
|
624
771
|
header: CheckpointHeader,
|
|
625
|
-
|
|
772
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
626
773
|
): Promise<void> {
|
|
627
774
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
628
775
|
|
|
629
776
|
const args = [
|
|
630
777
|
header.toViem(),
|
|
631
|
-
CommitteeAttestationsAndSigners.
|
|
778
|
+
CommitteeAttestationsAndSigners.packAttestations([]),
|
|
632
779
|
[], // no signers
|
|
633
780
|
Signature.empty().toViemSignature(),
|
|
634
781
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
@@ -636,21 +783,13 @@ export class SequencerPublisher {
|
|
|
636
783
|
flags,
|
|
637
784
|
] as const;
|
|
638
785
|
|
|
639
|
-
const
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
let balance = 0n;
|
|
644
|
-
if (this.config.fishermanMode) {
|
|
645
|
-
// In fisherman mode, we can't know where the proposer is publishing from
|
|
646
|
-
// so we just add sufficient balance to the multicall3 address
|
|
647
|
-
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
648
|
-
} else {
|
|
649
|
-
balance = await this.l1TxUtils.getSenderBalance();
|
|
650
|
-
}
|
|
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.
|
|
651
790
|
stateOverrides.push({
|
|
652
791
|
address: MULTI_CALL_3_ADDRESS,
|
|
653
|
-
balance,
|
|
792
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
654
793
|
});
|
|
655
794
|
|
|
656
795
|
await this.l1TxUtils.simulate(
|
|
@@ -659,7 +798,7 @@ export class SequencerPublisher {
|
|
|
659
798
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
660
799
|
from: MULTI_CALL_3_ADDRESS,
|
|
661
800
|
},
|
|
662
|
-
{ time: ts
|
|
801
|
+
{ time: ts },
|
|
663
802
|
stateOverrides,
|
|
664
803
|
);
|
|
665
804
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -712,6 +851,7 @@ export class SequencerPublisher {
|
|
|
712
851
|
gasUsed,
|
|
713
852
|
checkpointNumber,
|
|
714
853
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
854
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
715
855
|
reason,
|
|
716
856
|
};
|
|
717
857
|
} catch (err) {
|
|
@@ -724,8 +864,8 @@ export class SequencerPublisher {
|
|
|
724
864
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
725
865
|
{ ...logData, request, error: viemError.message },
|
|
726
866
|
);
|
|
727
|
-
const
|
|
728
|
-
if (
|
|
867
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
868
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
729
869
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
730
870
|
return undefined;
|
|
731
871
|
} else {
|
|
@@ -770,9 +910,11 @@ export class SequencerPublisher {
|
|
|
770
910
|
const logData = { ...checkpoint, reason };
|
|
771
911
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
772
912
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
)
|
|
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;
|
|
776
918
|
|
|
777
919
|
if (reason === 'invalid-attestation') {
|
|
778
920
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -793,43 +935,11 @@ export class SequencerPublisher {
|
|
|
793
935
|
}
|
|
794
936
|
}
|
|
795
937
|
|
|
796
|
-
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
797
|
-
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
798
|
-
public async validateCheckpointForSubmission(
|
|
799
|
-
checkpoint: Checkpoint,
|
|
800
|
-
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
801
|
-
attestationsAndSignersSignature: Signature,
|
|
802
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
803
|
-
): Promise<bigint> {
|
|
804
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
805
|
-
const blobFields = checkpoint.toBlobFields();
|
|
806
|
-
const blobs = await getBlobsPerL1Block(blobFields);
|
|
807
|
-
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
808
|
-
|
|
809
|
-
const args = [
|
|
810
|
-
{
|
|
811
|
-
header: checkpoint.header.toViem(),
|
|
812
|
-
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
813
|
-
oracleInput: {
|
|
814
|
-
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
815
|
-
},
|
|
816
|
-
},
|
|
817
|
-
attestationsAndSigners.getPackedAttestations(),
|
|
818
|
-
attestationsAndSigners.getSigners().map(signer => signer.toString()),
|
|
819
|
-
attestationsAndSignersSignature.toViemSignature(),
|
|
820
|
-
blobInput,
|
|
821
|
-
] as const;
|
|
822
|
-
|
|
823
|
-
await this.simulateProposeTx(args, ts, options);
|
|
824
|
-
return ts;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
938
|
private async enqueueCastSignalHelper(
|
|
828
939
|
slotNumber: SlotNumber,
|
|
829
|
-
timestamp: bigint,
|
|
830
940
|
signalType: GovernanceSignalAction,
|
|
831
941
|
payload: EthAddress,
|
|
832
|
-
base:
|
|
942
|
+
base: GovernanceProposerContract,
|
|
833
943
|
signerAddress: EthAddress,
|
|
834
944
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
835
945
|
): Promise<boolean> {
|
|
@@ -844,6 +954,19 @@ export class SequencerPublisher {
|
|
|
844
954
|
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
845
955
|
return false;
|
|
846
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
|
+
|
|
847
970
|
const round = await base.computeRound(slotNumber);
|
|
848
971
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
849
972
|
|
|
@@ -855,34 +978,43 @@ export class SequencerPublisher {
|
|
|
855
978
|
return false;
|
|
856
979
|
}
|
|
857
980
|
|
|
858
|
-
if (await
|
|
981
|
+
if (await base.isPayloadEmpty(payload)) {
|
|
859
982
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
860
983
|
return false;
|
|
861
984
|
}
|
|
862
985
|
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
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;
|
|
882
1010
|
}
|
|
883
1011
|
|
|
884
|
-
if (this.
|
|
885
|
-
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
|
+
);
|
|
886
1018
|
return false;
|
|
887
1019
|
}
|
|
888
1020
|
|
|
@@ -904,41 +1036,19 @@ export class SequencerPublisher {
|
|
|
904
1036
|
lastValidL2Slot: slotNumber,
|
|
905
1037
|
});
|
|
906
1038
|
|
|
907
|
-
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
908
|
-
|
|
909
|
-
try {
|
|
910
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
911
|
-
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
912
|
-
} catch (err) {
|
|
913
|
-
const viemError = formatViemError(err);
|
|
914
|
-
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
915
|
-
this.backupFailedTx({
|
|
916
|
-
id: keccak256(request.data!),
|
|
917
|
-
failureType: 'simulation',
|
|
918
|
-
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
919
|
-
l1BlockNumber: l1BlockNumber.toString(),
|
|
920
|
-
error: { message: viemError.message, name: viemError.name },
|
|
921
|
-
context: {
|
|
922
|
-
actions: [action],
|
|
923
|
-
slot: slotNumber,
|
|
924
|
-
sender: this.getSenderAddress().toString(),
|
|
925
|
-
},
|
|
926
|
-
});
|
|
927
|
-
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
928
|
-
}
|
|
929
|
-
|
|
930
1039
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
931
1040
|
this.addRequest({
|
|
932
|
-
gasConfig: { gasLimit: SequencerPublisher.VOTE_GAS_GUESS },
|
|
933
1041
|
action,
|
|
934
1042
|
request,
|
|
935
1043
|
lastValidL2Slot: slotNumber,
|
|
936
1044
|
checkSuccess: (_request, result) => {
|
|
937
1045
|
const success =
|
|
938
1046
|
result &&
|
|
939
|
-
result.receipt
|
|
940
|
-
|
|
941
|
-
|
|
1047
|
+
extractEventSuccess(result.receipt, {
|
|
1048
|
+
address: base.address.toString(),
|
|
1049
|
+
abi: EmpireBaseAbi,
|
|
1050
|
+
eventName: 'SignalCast',
|
|
1051
|
+
});
|
|
942
1052
|
|
|
943
1053
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
944
1054
|
if (!success) {
|
|
@@ -960,33 +1070,19 @@ export class SequencerPublisher {
|
|
|
960
1070
|
return true;
|
|
961
1071
|
}
|
|
962
1072
|
|
|
963
|
-
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
964
|
-
const key = payload.toString();
|
|
965
|
-
const cached = this.isPayloadEmptyCache.get(key);
|
|
966
|
-
if (cached) {
|
|
967
|
-
return cached;
|
|
968
|
-
}
|
|
969
|
-
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
970
|
-
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
971
|
-
return isEmpty;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
1073
|
/**
|
|
975
1074
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
976
1075
|
* @param slotNumber - The slot number to cast a signal for.
|
|
977
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
978
1076
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
979
1077
|
*/
|
|
980
1078
|
public enqueueGovernanceCastSignal(
|
|
981
1079
|
governancePayload: EthAddress,
|
|
982
1080
|
slotNumber: SlotNumber,
|
|
983
|
-
timestamp: bigint,
|
|
984
1081
|
signerAddress: EthAddress,
|
|
985
1082
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
986
1083
|
): Promise<boolean> {
|
|
987
1084
|
return this.enqueueCastSignalHelper(
|
|
988
1085
|
slotNumber,
|
|
989
|
-
timestamp,
|
|
990
1086
|
'governance-signal',
|
|
991
1087
|
governancePayload,
|
|
992
1088
|
this.govProposerContract,
|
|
@@ -995,11 +1091,51 @@ export class SequencerPublisher {
|
|
|
995
1091
|
);
|
|
996
1092
|
}
|
|
997
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
|
+
|
|
998
1135
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
999
1136
|
public async enqueueSlashingActions(
|
|
1000
1137
|
actions: ProposerSlashAction[],
|
|
1001
1138
|
slotNumber: SlotNumber,
|
|
1002
|
-
timestamp: bigint,
|
|
1003
1139
|
signerAddress: EthAddress,
|
|
1004
1140
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
1005
1141
|
): Promise<boolean> {
|
|
@@ -1010,58 +1146,6 @@ export class SequencerPublisher {
|
|
|
1010
1146
|
|
|
1011
1147
|
for (const action of actions) {
|
|
1012
1148
|
switch (action.type) {
|
|
1013
|
-
case 'vote-empire-payload': {
|
|
1014
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1015
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
1016
|
-
break;
|
|
1017
|
-
}
|
|
1018
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
1019
|
-
signerAddress,
|
|
1020
|
-
});
|
|
1021
|
-
await this.enqueueCastSignalHelper(
|
|
1022
|
-
slotNumber,
|
|
1023
|
-
timestamp,
|
|
1024
|
-
'empire-slashing-signal',
|
|
1025
|
-
action.payload,
|
|
1026
|
-
this.slashingProposerContract,
|
|
1027
|
-
signerAddress,
|
|
1028
|
-
signer,
|
|
1029
|
-
);
|
|
1030
|
-
break;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
case 'create-empire-payload': {
|
|
1034
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
1035
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
1036
|
-
await this.simulateAndEnqueueRequest(
|
|
1037
|
-
'create-empire-payload',
|
|
1038
|
-
request,
|
|
1039
|
-
(receipt: TransactionReceipt) =>
|
|
1040
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
1041
|
-
slotNumber,
|
|
1042
|
-
timestamp,
|
|
1043
|
-
);
|
|
1044
|
-
break;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
case 'execute-empire-payload': {
|
|
1048
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
1049
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
1050
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
1054
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
1055
|
-
await this.simulateAndEnqueueRequest(
|
|
1056
|
-
'execute-empire-payload',
|
|
1057
|
-
request,
|
|
1058
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
1059
|
-
slotNumber,
|
|
1060
|
-
timestamp,
|
|
1061
|
-
);
|
|
1062
|
-
break;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
1149
|
case 'vote-offenses': {
|
|
1066
1150
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
1067
1151
|
slotNumber,
|
|
@@ -1069,19 +1153,21 @@ export class SequencerPublisher {
|
|
|
1069
1153
|
votesCount: action.votes.length,
|
|
1070
1154
|
signerAddress,
|
|
1071
1155
|
});
|
|
1072
|
-
if (this.slashingProposerContract
|
|
1073
|
-
this.log.error('
|
|
1156
|
+
if (!this.slashingProposerContract) {
|
|
1157
|
+
this.log.error('No slashing proposer contract available');
|
|
1074
1158
|
return false;
|
|
1075
1159
|
}
|
|
1076
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
1077
1160
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
1078
|
-
const request = await
|
|
1079
|
-
|
|
1161
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1162
|
+
this.enqueueRequest(
|
|
1080
1163
|
'vote-offenses',
|
|
1081
1164
|
request,
|
|
1082
|
-
|
|
1165
|
+
{
|
|
1166
|
+
address: this.slashingProposerContract.address.toString(),
|
|
1167
|
+
abi: SlashingProposerAbi,
|
|
1168
|
+
eventName: 'VoteCast',
|
|
1169
|
+
},
|
|
1083
1170
|
slotNumber,
|
|
1084
|
-
timestamp,
|
|
1085
1171
|
);
|
|
1086
1172
|
break;
|
|
1087
1173
|
}
|
|
@@ -1092,18 +1178,23 @@ export class SequencerPublisher {
|
|
|
1092
1178
|
round: action.round,
|
|
1093
1179
|
signerAddress,
|
|
1094
1180
|
});
|
|
1095
|
-
if (this.slashingProposerContract
|
|
1096
|
-
this.log.error('
|
|
1181
|
+
if (!this.slashingProposerContract) {
|
|
1182
|
+
this.log.error('No slashing proposer contract available');
|
|
1097
1183
|
return false;
|
|
1098
1184
|
}
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1185
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1186
|
+
action.round,
|
|
1187
|
+
action.committees,
|
|
1188
|
+
);
|
|
1189
|
+
this.enqueueRequest(
|
|
1102
1190
|
'execute-slash',
|
|
1103
|
-
|
|
1104
|
-
|
|
1191
|
+
executeRequest,
|
|
1192
|
+
{
|
|
1193
|
+
address: this.slashingProposerContract.address.toString(),
|
|
1194
|
+
abi: SlashingProposerAbi,
|
|
1195
|
+
eventName: 'RoundExecuted',
|
|
1196
|
+
},
|
|
1105
1197
|
slotNumber,
|
|
1106
|
-
timestamp,
|
|
1107
1198
|
);
|
|
1108
1199
|
break;
|
|
1109
1200
|
}
|
|
@@ -1118,12 +1209,12 @@ export class SequencerPublisher {
|
|
|
1118
1209
|
return true;
|
|
1119
1210
|
}
|
|
1120
1211
|
|
|
1121
|
-
/**
|
|
1212
|
+
/** Enqueues a proposal for a checkpoint on L1 */
|
|
1122
1213
|
public async enqueueProposeCheckpoint(
|
|
1123
1214
|
checkpoint: Checkpoint,
|
|
1124
1215
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
1125
1216
|
attestationsAndSignersSignature: Signature,
|
|
1126
|
-
opts:
|
|
1217
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1127
1218
|
): Promise<void> {
|
|
1128
1219
|
const checkpointHeader = checkpoint.header;
|
|
1129
1220
|
|
|
@@ -1139,31 +1230,11 @@ export class SequencerPublisher {
|
|
|
1139
1230
|
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
1140
1231
|
};
|
|
1141
1232
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
1148
|
-
// make time consistency checks break.
|
|
1149
|
-
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
1150
|
-
ts = await this.validateCheckpointForSubmission(
|
|
1151
|
-
checkpoint,
|
|
1152
|
-
attestationsAndSigners,
|
|
1153
|
-
attestationsAndSignersSignature,
|
|
1154
|
-
opts,
|
|
1155
|
-
);
|
|
1156
|
-
} catch (err: any) {
|
|
1157
|
-
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1158
|
-
...checkpoint.getStats(),
|
|
1159
|
-
slotNumber: checkpoint.header.slotNumber,
|
|
1160
|
-
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
1161
|
-
});
|
|
1162
|
-
throw err;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1166
|
-
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
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 });
|
|
1167
1238
|
}
|
|
1168
1239
|
|
|
1169
1240
|
public enqueueInvalidateCheckpoint(
|
|
@@ -1174,23 +1245,22 @@ export class SequencerPublisher {
|
|
|
1174
1245
|
return;
|
|
1175
1246
|
}
|
|
1176
1247
|
|
|
1177
|
-
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1178
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
1179
|
-
|
|
1180
1248
|
const { gasUsed, checkpointNumber } = request;
|
|
1181
|
-
const logData = { gasUsed, checkpointNumber,
|
|
1249
|
+
const logData = { gasUsed, checkpointNumber, opts };
|
|
1182
1250
|
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
1183
1251
|
this.addRequest({
|
|
1184
1252
|
action: `invalidate-by-${request.reason}`,
|
|
1185
1253
|
request: request.request,
|
|
1186
|
-
gasConfig: {
|
|
1254
|
+
gasConfig: opts.txTimeoutAt ? { txTimeoutAt: opts.txTimeoutAt } : undefined,
|
|
1187
1255
|
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
1188
1256
|
checkSuccess: (_req, result) => {
|
|
1189
1257
|
const success =
|
|
1190
1258
|
result &&
|
|
1191
|
-
result.receipt
|
|
1192
|
-
|
|
1193
|
-
|
|
1259
|
+
extractEventSuccess(result.receipt, {
|
|
1260
|
+
address: this.rollupContract.address,
|
|
1261
|
+
abi: RollupAbi,
|
|
1262
|
+
eventName: 'CheckpointInvalidated',
|
|
1263
|
+
});
|
|
1194
1264
|
if (!success) {
|
|
1195
1265
|
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
1196
1266
|
} else {
|
|
@@ -1201,72 +1271,36 @@ export class SequencerPublisher {
|
|
|
1201
1271
|
});
|
|
1202
1272
|
}
|
|
1203
1273
|
|
|
1204
|
-
|
|
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(
|
|
1205
1280
|
action: Action,
|
|
1206
1281
|
request: L1TxRequest,
|
|
1207
|
-
|
|
1282
|
+
eventOpts: { address: string; abi: Abi; eventName: string },
|
|
1208
1283
|
slotNumber: SlotNumber,
|
|
1209
|
-
|
|
1210
|
-
) {
|
|
1211
|
-
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
1284
|
+
): boolean {
|
|
1212
1285
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
1213
1286
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
1214
1287
|
return false;
|
|
1215
1288
|
}
|
|
1216
|
-
|
|
1217
1289
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1218
1290
|
this.lastActions[action] = slotNumber;
|
|
1219
1291
|
|
|
1220
|
-
this.log.debug(`
|
|
1221
|
-
|
|
1222
|
-
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1223
|
-
|
|
1224
|
-
let gasUsed: bigint;
|
|
1225
|
-
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1226
|
-
try {
|
|
1227
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
1228
|
-
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1229
|
-
} catch (err) {
|
|
1230
|
-
const viemError = formatViemError(err, simulateAbi);
|
|
1231
|
-
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1232
|
-
|
|
1233
|
-
this.backupFailedTx({
|
|
1234
|
-
id: keccak256(request.data!),
|
|
1235
|
-
failureType: 'simulation',
|
|
1236
|
-
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1237
|
-
l1BlockNumber: l1BlockNumber.toString(),
|
|
1238
|
-
error: { message: viemError.message, name: viemError.name },
|
|
1239
|
-
context: {
|
|
1240
|
-
actions: [action],
|
|
1241
|
-
slot: slotNumber,
|
|
1242
|
-
sender: this.getSenderAddress().toString(),
|
|
1243
|
-
},
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1250
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1251
|
-
logData.gasLimit = gasLimit;
|
|
1252
|
-
|
|
1253
|
-
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1254
|
-
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1255
|
-
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1256
|
-
|
|
1257
|
-
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1292
|
+
this.log.debug(`Enqueuing ${action}`, { slotNumber });
|
|
1258
1293
|
this.addRequest({
|
|
1259
1294
|
action,
|
|
1260
|
-
request
|
|
1261
|
-
gasConfig: { gasLimit },
|
|
1295
|
+
request,
|
|
1262
1296
|
lastValidL2Slot: slotNumber,
|
|
1263
|
-
checkSuccess: (
|
|
1264
|
-
const success = result && result.receipt
|
|
1297
|
+
checkSuccess: (_request, result) => {
|
|
1298
|
+
const success = result && extractEventSuccess(result.receipt, eventOpts);
|
|
1265
1299
|
if (!success) {
|
|
1266
|
-
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result,
|
|
1300
|
+
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result, slotNumber });
|
|
1267
1301
|
this.lastActions[action] = cachedLastActionSlot;
|
|
1268
1302
|
} else {
|
|
1269
|
-
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result,
|
|
1303
|
+
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result, slotNumber });
|
|
1270
1304
|
}
|
|
1271
1305
|
return !!success;
|
|
1272
1306
|
},
|
|
@@ -1282,6 +1316,7 @@ export class SequencerPublisher {
|
|
|
1282
1316
|
*/
|
|
1283
1317
|
public interrupt() {
|
|
1284
1318
|
this.interrupted = true;
|
|
1319
|
+
this.interruptibleSleep.interrupt();
|
|
1285
1320
|
this.l1TxUtils.interrupt();
|
|
1286
1321
|
}
|
|
1287
1322
|
|
|
@@ -1291,11 +1326,7 @@ export class SequencerPublisher {
|
|
|
1291
1326
|
this.l1TxUtils.restart();
|
|
1292
1327
|
}
|
|
1293
1328
|
|
|
1294
|
-
private async prepareProposeTx(
|
|
1295
|
-
encodedData: L1ProcessArgs,
|
|
1296
|
-
timestamp: bigint,
|
|
1297
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1298
|
-
) {
|
|
1329
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs) {
|
|
1299
1330
|
const kzg = Blob.getViemKzgInstance();
|
|
1300
1331
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1301
1332
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1308,7 +1339,11 @@ export class SequencerPublisher {
|
|
|
1308
1339
|
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
1309
1340
|
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
1310
1341
|
} else {
|
|
1311
|
-
//
|
|
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.
|
|
1312
1347
|
blobEvaluationGas = await this.l1TxUtils
|
|
1313
1348
|
.estimateGas(
|
|
1314
1349
|
this.getSenderAddress().toString(),
|
|
@@ -1366,138 +1401,21 @@ export class SequencerPublisher {
|
|
|
1366
1401
|
blobInput,
|
|
1367
1402
|
] as const;
|
|
1368
1403
|
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/**
|
|
1375
|
-
* Simulates the propose tx with eth_simulateV1
|
|
1376
|
-
* @param args - The propose tx args
|
|
1377
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1378
|
-
* @returns The simulation result
|
|
1379
|
-
*/
|
|
1380
|
-
private async simulateProposeTx(
|
|
1381
|
-
args: readonly [
|
|
1382
|
-
{
|
|
1383
|
-
readonly header: ViemHeader;
|
|
1384
|
-
readonly archive: `0x${string}`;
|
|
1385
|
-
readonly oracleInput: {
|
|
1386
|
-
readonly feeAssetPriceModifier: bigint;
|
|
1387
|
-
};
|
|
1388
|
-
},
|
|
1389
|
-
ViemCommitteeAttestations,
|
|
1390
|
-
`0x${string}`[], // Signers
|
|
1391
|
-
ViemSignature,
|
|
1392
|
-
`0x${string}`,
|
|
1393
|
-
],
|
|
1394
|
-
timestamp: bigint,
|
|
1395
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1396
|
-
) {
|
|
1397
|
-
const rollupData = encodeFunctionData({
|
|
1398
|
-
abi: RollupAbi,
|
|
1399
|
-
functionName: 'propose',
|
|
1400
|
-
args,
|
|
1401
|
-
});
|
|
1402
|
-
|
|
1403
|
-
// override the pending checkpoint number if requested
|
|
1404
|
-
const forcePendingCheckpointNumberStateDiff = (
|
|
1405
|
-
options.forcePendingCheckpointNumber !== undefined
|
|
1406
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1407
|
-
: []
|
|
1408
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
1409
|
-
|
|
1410
|
-
const stateOverrides: StateOverride = [
|
|
1411
|
-
{
|
|
1412
|
-
address: this.rollupContract.address,
|
|
1413
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1414
|
-
stateDiff: [
|
|
1415
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1416
|
-
...forcePendingCheckpointNumberStateDiff,
|
|
1417
|
-
],
|
|
1418
|
-
},
|
|
1419
|
-
];
|
|
1420
|
-
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1421
|
-
if (this.proposerAddressForSimulation) {
|
|
1422
|
-
stateOverrides.push({
|
|
1423
|
-
address: this.proposerAddressForSimulation.toString(),
|
|
1424
|
-
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1429
|
-
|
|
1430
|
-
const simulationResult = await this.l1TxUtils
|
|
1431
|
-
.simulate(
|
|
1432
|
-
{
|
|
1433
|
-
to: this.rollupContract.address,
|
|
1434
|
-
data: rollupData,
|
|
1435
|
-
gas: MAX_L1_TX_LIMIT,
|
|
1436
|
-
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1437
|
-
},
|
|
1438
|
-
{
|
|
1439
|
-
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1440
|
-
time: timestamp + 1n,
|
|
1441
|
-
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1442
|
-
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1443
|
-
},
|
|
1444
|
-
stateOverrides,
|
|
1445
|
-
RollupAbi,
|
|
1446
|
-
{
|
|
1447
|
-
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1448
|
-
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1449
|
-
},
|
|
1450
|
-
)
|
|
1451
|
-
.catch(err => {
|
|
1452
|
-
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1453
|
-
const viemError = formatViemError(err);
|
|
1454
|
-
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1455
|
-
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1456
|
-
// Return a minimal simulation result with the fallback gas estimate
|
|
1457
|
-
return {
|
|
1458
|
-
gasUsed: MAX_L1_TX_LIMIT,
|
|
1459
|
-
logs: [],
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1463
|
-
this.backupFailedTx({
|
|
1464
|
-
id: keccak256(rollupData),
|
|
1465
|
-
failureType: 'simulation',
|
|
1466
|
-
request: { to: this.rollupContract.address, data: rollupData },
|
|
1467
|
-
l1BlockNumber: l1BlockNumber.toString(),
|
|
1468
|
-
error: { message: viemError.message, name: viemError.name },
|
|
1469
|
-
context: {
|
|
1470
|
-
actions: ['propose'],
|
|
1471
|
-
slot: Number(args[0].header.slotNumber),
|
|
1472
|
-
sender: this.getSenderAddress().toString(),
|
|
1473
|
-
},
|
|
1474
|
-
});
|
|
1475
|
-
throw err;
|
|
1476
|
-
});
|
|
1404
|
+
const rollupData = encodeFunctionData({ abi: RollupAbi, functionName: 'propose', args });
|
|
1477
1405
|
|
|
1478
|
-
return {
|
|
1406
|
+
return { args, blobEvaluationGas, rollupData };
|
|
1479
1407
|
}
|
|
1480
1408
|
|
|
1481
1409
|
private async addProposeTx(
|
|
1482
1410
|
checkpoint: Checkpoint,
|
|
1483
1411
|
encodedData: L1ProcessArgs,
|
|
1484
|
-
opts:
|
|
1485
|
-
timestamp: bigint,
|
|
1412
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1486
1413
|
): Promise<void> {
|
|
1487
1414
|
const slot = checkpoint.header.slotNumber;
|
|
1488
1415
|
const timer = new Timer();
|
|
1489
1416
|
const kzg = Blob.getViemKzgInstance();
|
|
1490
|
-
const { rollupData,
|
|
1491
|
-
encodedData,
|
|
1492
|
-
timestamp,
|
|
1493
|
-
opts,
|
|
1494
|
-
);
|
|
1417
|
+
const { rollupData, blobEvaluationGas } = await this.prepareProposeTx(encodedData);
|
|
1495
1418
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1496
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
1497
|
-
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
1498
|
-
blobEvaluationGas +
|
|
1499
|
-
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1500
|
-
);
|
|
1501
1419
|
|
|
1502
1420
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1503
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.
|
|
@@ -1514,7 +1432,8 @@ export class SequencerPublisher {
|
|
|
1514
1432
|
data: rollupData,
|
|
1515
1433
|
},
|
|
1516
1434
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1517
|
-
gasConfig: {
|
|
1435
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit: undefined },
|
|
1436
|
+
blobEvaluationGas,
|
|
1518
1437
|
blobConfig: {
|
|
1519
1438
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1520
1439
|
kzg,
|
|
@@ -1524,10 +1443,11 @@ export class SequencerPublisher {
|
|
|
1524
1443
|
return false;
|
|
1525
1444
|
}
|
|
1526
1445
|
const { receipt, stats, errorMsg } = result;
|
|
1527
|
-
const success =
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1446
|
+
const success = extractEventSuccess(receipt, {
|
|
1447
|
+
address: this.rollupContract.address,
|
|
1448
|
+
abi: RollupAbi,
|
|
1449
|
+
eventName: 'CheckpointProposed',
|
|
1450
|
+
});
|
|
1531
1451
|
|
|
1532
1452
|
if (success) {
|
|
1533
1453
|
const endBlock = receipt.blockNumber;
|
|
@@ -1567,4 +1487,10 @@ export class SequencerPublisher {
|
|
|
1567
1487
|
},
|
|
1568
1488
|
});
|
|
1569
1489
|
}
|
|
1490
|
+
|
|
1491
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1492
|
+
private getNextL1SlotTimestamp(): bigint {
|
|
1493
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1494
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1495
|
+
}
|
|
1570
1496
|
}
|