@aztec/sequencer-client 0.0.1-commit.f2ce05ee → 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 +34 -11
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +77 -39
- package/dest/config.d.ts +34 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +103 -39
- 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 -28
- 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 +51 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +131 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- 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/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-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 +11 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -3
- package/dest/publisher/sequencer-publisher.d.ts +112 -71
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +571 -392
- 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 +886 -264
- 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 +23 -8
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +117 -21
- 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 +181 -44
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +635 -218
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +5 -7
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +17 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +28 -9
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +110 -47
- package/src/config.ts +126 -44
- 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 +20 -89
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +173 -40
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_to_l2_messaging.ts +85 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -9
- package/src/publisher/sequencer-publisher.ts +667 -484
- 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 +1073 -290
- 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 +138 -26
- package/src/sequencer/requests_tracker.ts +43 -0
- package/src/sequencer/sequencer.ts +745 -243
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +4 -6
- package/src/test/mock_checkpoint_builder.ts +63 -49
- package/src/test/utils.ts +64 -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,51 +3,103 @@ 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
|
-
|
|
6
|
+
FeeAssetPriceOracle,
|
|
7
7
|
type GovernanceProposerContract,
|
|
8
|
-
type IEmpireBase,
|
|
9
8
|
MULTI_CALL_3_ADDRESS,
|
|
10
9
|
Multicall3,
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
type
|
|
10
|
+
MulticallForwarderRevertedError,
|
|
11
|
+
type PayloadProposalStatus,
|
|
12
|
+
type RollupContract,
|
|
13
|
+
type SimulationOverridesPlan,
|
|
14
|
+
type SlashingProposerContract,
|
|
15
|
+
buildSimulationOverridesStateOverride,
|
|
15
16
|
} from '@aztec/ethereum/contracts';
|
|
16
17
|
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
18
|
import {
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
21
23
|
MAX_L1_TX_LIMIT,
|
|
22
24
|
type TransactionStats,
|
|
23
25
|
WEI_CONST,
|
|
24
26
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
import {
|
|
28
|
+
FormattedViemError,
|
|
29
|
+
formatViemError,
|
|
30
|
+
mergeAbis,
|
|
31
|
+
tryDecodeRevertReason,
|
|
32
|
+
tryExtractEvent,
|
|
33
|
+
} from '@aztec/ethereum/utils';
|
|
29
34
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
35
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
30
36
|
import { pick } from '@aztec/foundation/collection';
|
|
31
37
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
38
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
32
39
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
|
-
import { Signature
|
|
40
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
34
41
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
42
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
35
43
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
|
-
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';
|
|
38
46
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
39
47
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
40
48
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
41
|
-
import {
|
|
49
|
+
import type { SequencerConfig } from '@aztec/stdlib/config';
|
|
50
|
+
import {
|
|
51
|
+
getLastL1SlotTimestampForL2Slot,
|
|
52
|
+
getNextL1SlotTimestamp,
|
|
53
|
+
getTimestampForSlot,
|
|
54
|
+
} from '@aztec/stdlib/epoch-helpers';
|
|
42
55
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
56
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
57
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
45
58
|
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
import {
|
|
60
|
+
type Abi,
|
|
61
|
+
type Hex,
|
|
62
|
+
type TransactionReceipt,
|
|
63
|
+
type TypedDataDefinition,
|
|
64
|
+
encodeFunctionData,
|
|
65
|
+
keccak256,
|
|
66
|
+
toHex,
|
|
67
|
+
} from 'viem';
|
|
68
|
+
|
|
69
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
70
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
71
|
+
import { type DroppedRequest, SequencerBundleSimulator } from './sequencer-bundle-simulator.js';
|
|
49
72
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
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
|
+
|
|
51
103
|
/** Arguments to the process method of the rollup contract */
|
|
52
104
|
type L1ProcessArgs = {
|
|
53
105
|
/** The L2 block header. */
|
|
@@ -60,23 +112,23 @@ type L1ProcessArgs = {
|
|
|
60
112
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
113
|
/** Attestations and signers signature */
|
|
62
114
|
attestationsAndSignersSignature: Signature;
|
|
115
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
116
|
+
feeAssetPriceModifier: bigint;
|
|
63
117
|
};
|
|
64
118
|
|
|
65
119
|
export const Actions = [
|
|
66
120
|
'invalidate-by-invalid-attestation',
|
|
67
121
|
'invalidate-by-insufficient-attestations',
|
|
122
|
+
'prune',
|
|
68
123
|
'propose',
|
|
69
124
|
'governance-signal',
|
|
70
|
-
'empire-slashing-signal',
|
|
71
|
-
'create-empire-payload',
|
|
72
|
-
'execute-empire-payload',
|
|
73
125
|
'vote-offenses',
|
|
74
126
|
'execute-slash',
|
|
75
127
|
] as const;
|
|
76
128
|
|
|
77
129
|
export type Action = (typeof Actions)[number];
|
|
78
130
|
|
|
79
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
131
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
80
132
|
|
|
81
133
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
82
134
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -87,14 +139,22 @@ export type InvalidateCheckpointRequest = {
|
|
|
87
139
|
gasUsed: bigint;
|
|
88
140
|
checkpointNumber: CheckpointNumber;
|
|
89
141
|
forcePendingCheckpointNumber: CheckpointNumber;
|
|
142
|
+
/** Archive at the rollback target checkpoint (checkpoint N-1). */
|
|
143
|
+
lastArchive: Fr;
|
|
90
144
|
};
|
|
91
145
|
|
|
92
|
-
|
|
146
|
+
type EnqueueProposeCheckpointOpts = {
|
|
147
|
+
txTimeoutAt?: Date;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export interface RequestWithExpiry {
|
|
93
151
|
action: Action;
|
|
94
152
|
request: L1TxRequest;
|
|
95
153
|
lastValidL2Slot: SlotNumber;
|
|
96
154
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
97
155
|
blobConfig?: L1BlobInputs;
|
|
156
|
+
/** Gas consumed by validateBlobs; stashed for the bundle simulate at send time. */
|
|
157
|
+
blobEvaluationGas?: bigint;
|
|
98
158
|
checkSuccess: (
|
|
99
159
|
request: L1TxRequest,
|
|
100
160
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -104,69 +164,93 @@ interface RequestWithExpiry {
|
|
|
104
164
|
export class SequencerPublisher {
|
|
105
165
|
private interrupted = false;
|
|
106
166
|
private metrics: SequencerPublisherMetrics;
|
|
167
|
+
private bundleSimulator: SequencerBundleSimulator;
|
|
107
168
|
public epochCache: EpochCache;
|
|
169
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
108
170
|
|
|
109
|
-
|
|
110
|
-
|
|
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]);
|
|
111
177
|
|
|
112
178
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
179
|
|
|
114
|
-
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
115
|
-
|
|
116
180
|
protected log: Logger;
|
|
117
181
|
protected ethereumSlotDuration: bigint;
|
|
182
|
+
protected aztecSlotDuration: bigint;
|
|
183
|
+
private readonly previousL1BlockWaitTimeoutMs: number;
|
|
184
|
+
private readonly previousL1BlockWaitPollIntervalMs: number;
|
|
185
|
+
|
|
186
|
+
/** Date provider for wall-clock time. */
|
|
187
|
+
private readonly dateProvider: DateProvider;
|
|
118
188
|
|
|
119
189
|
private blobClient: BlobClientInterface;
|
|
120
190
|
|
|
121
|
-
/**
|
|
122
|
-
private
|
|
191
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
192
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
123
193
|
|
|
124
194
|
/** L1 fee analyzer for fisherman mode */
|
|
125
195
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
126
|
-
// A CALL to a cold address is 2700 gas
|
|
127
|
-
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
196
|
|
|
129
|
-
|
|
130
|
-
|
|
197
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
198
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
131
199
|
|
|
132
|
-
|
|
200
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
201
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
202
|
+
|
|
203
|
+
public l1TxUtils: L1TxUtils;
|
|
133
204
|
public rollupContract: RollupContract;
|
|
134
205
|
public govProposerContract: GovernanceProposerContract;
|
|
135
|
-
public slashingProposerContract:
|
|
136
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
206
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
137
207
|
|
|
138
208
|
public readonly tracer: Tracer;
|
|
139
209
|
|
|
140
210
|
protected requests: RequestWithExpiry[] = [];
|
|
141
211
|
|
|
142
212
|
constructor(
|
|
143
|
-
private config:
|
|
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 },
|
|
144
222
|
deps: {
|
|
145
223
|
telemetry?: TelemetryClient;
|
|
146
224
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
225
|
+
l1TxUtils: L1TxUtils;
|
|
148
226
|
rollupContract: RollupContract;
|
|
149
|
-
slashingProposerContract:
|
|
227
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
150
228
|
governanceProposerContract: GovernanceProposerContract;
|
|
151
|
-
slashFactoryContract: SlashFactoryContract;
|
|
152
229
|
epochCache: EpochCache;
|
|
153
230
|
dateProvider: DateProvider;
|
|
154
231
|
metrics: SequencerPublisherMetrics;
|
|
155
232
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
156
233
|
log?: Logger;
|
|
234
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
157
235
|
},
|
|
158
236
|
) {
|
|
159
237
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
160
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;
|
|
161
243
|
this.epochCache = deps.epochCache;
|
|
162
244
|
this.lastActions = deps.lastActions;
|
|
163
245
|
|
|
164
246
|
this.blobClient = deps.blobClient;
|
|
247
|
+
this.dateProvider = deps.dateProvider;
|
|
165
248
|
|
|
166
249
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
167
250
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
168
251
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
169
252
|
this.l1TxUtils = deps.l1TxUtils;
|
|
253
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
170
254
|
|
|
171
255
|
this.rollupContract = deps.rollupContract;
|
|
172
256
|
|
|
@@ -178,22 +262,71 @@ export class SequencerPublisher {
|
|
|
178
262
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
179
263
|
this.slashingProposerContract = newSlashingProposer;
|
|
180
264
|
});
|
|
181
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
182
|
-
|
|
183
265
|
// Initialize L1 fee analyzer for fisherman mode
|
|
184
266
|
if (config.fishermanMode) {
|
|
185
267
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
186
268
|
this.l1TxUtils.client,
|
|
187
269
|
deps.dateProvider,
|
|
188
|
-
|
|
270
|
+
this.log.createChild('fee-analyzer'),
|
|
189
271
|
);
|
|
190
272
|
}
|
|
273
|
+
|
|
274
|
+
// Initialize fee asset price oracle
|
|
275
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
276
|
+
this.l1TxUtils.client,
|
|
277
|
+
this.rollupContract,
|
|
278
|
+
this.log.createChild('price-oracle'),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
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
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
294
|
+
* Does nothing if no store is configured.
|
|
295
|
+
*/
|
|
296
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
297
|
+
if (!this.failedTxStore) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const tx: FailedL1Tx = {
|
|
302
|
+
...failedTx,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Fire and forget - don't block on backup
|
|
307
|
+
void this.failedTxStore
|
|
308
|
+
.then(store => store?.saveFailedTx(tx))
|
|
309
|
+
.catch(err => {
|
|
310
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
311
|
+
});
|
|
191
312
|
}
|
|
192
313
|
|
|
193
314
|
public getRollupContract(): RollupContract {
|
|
194
315
|
return this.rollupContract;
|
|
195
316
|
}
|
|
196
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Gets the fee asset price modifier from the oracle.
|
|
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.
|
|
325
|
+
*/
|
|
326
|
+
public getFeeAssetPriceModifier(predictedParentEthPerFeeAssetE12?: bigint): Promise<bigint> {
|
|
327
|
+
return this.feeAssetPriceOracle.computePriceModifier(predictedParentEthPerFeeAssetE12);
|
|
328
|
+
}
|
|
329
|
+
|
|
197
330
|
public getSenderAddress() {
|
|
198
331
|
return this.l1TxUtils.getSenderAddress();
|
|
199
332
|
}
|
|
@@ -205,20 +338,12 @@ export class SequencerPublisher {
|
|
|
205
338
|
return this.l1FeeAnalyzer;
|
|
206
339
|
}
|
|
207
340
|
|
|
208
|
-
/**
|
|
209
|
-
* Sets the proposer address to use for simulations in fisherman mode.
|
|
210
|
-
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
211
|
-
*/
|
|
212
|
-
public setProposerAddressForSimulation(proposerAddress: EthAddress | undefined) {
|
|
213
|
-
this.proposerAddressForSimulation = proposerAddress;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
341
|
public addRequest(request: RequestWithExpiry) {
|
|
217
342
|
this.requests.push(request);
|
|
218
343
|
}
|
|
219
344
|
|
|
220
345
|
public getCurrentL2Slot(): SlotNumber {
|
|
221
|
-
return this.epochCache.
|
|
346
|
+
return this.epochCache.getSlotNow();
|
|
222
347
|
}
|
|
223
348
|
|
|
224
349
|
/**
|
|
@@ -290,22 +415,26 @@ export class SequencerPublisher {
|
|
|
290
415
|
|
|
291
416
|
/**
|
|
292
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.
|
|
293
422
|
* @returns one of:
|
|
294
423
|
* - A receipt and stats if the tx succeeded
|
|
295
424
|
* - a receipt and errorMsg if it failed on L1
|
|
296
425
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
297
426
|
*/
|
|
298
427
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
299
|
-
public async sendRequests() {
|
|
428
|
+
public async sendRequests(targetSlot?: SlotNumber): Promise<SendRequestsResult | undefined> {
|
|
300
429
|
const requestsToProcess = [...this.requests];
|
|
301
430
|
this.requests = [];
|
|
431
|
+
|
|
302
432
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
303
433
|
return undefined;
|
|
304
434
|
}
|
|
305
|
-
const currentL2Slot = this.getCurrentL2Slot();
|
|
435
|
+
const currentL2Slot = targetSlot ?? this.getCurrentL2Slot();
|
|
306
436
|
this.log.debug(`Sending requests on L2 slot ${currentL2Slot}`);
|
|
307
437
|
const validRequests = requestsToProcess.filter(request => request.lastValidL2Slot >= currentL2Slot);
|
|
308
|
-
const validActions = validRequests.map(x => x.action);
|
|
309
438
|
const expiredActions = requestsToProcess
|
|
310
439
|
.filter(request => request.lastValidL2Slot < currentL2Slot)
|
|
311
440
|
.map(x => x.action);
|
|
@@ -328,53 +457,60 @@ export class SequencerPublisher {
|
|
|
328
457
|
return undefined;
|
|
329
458
|
}
|
|
330
459
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
334
|
-
const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
335
|
-
const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
336
|
-
|
|
337
|
-
if (blobConfigs.length > 1) {
|
|
338
|
-
throw new Error('Multiple blob configs found');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const blobConfig = blobConfigs[0];
|
|
342
|
-
|
|
343
|
-
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
344
|
-
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
345
|
-
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
346
|
-
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
347
|
-
const maxGas = MAX_L1_TX_LIMIT;
|
|
348
|
-
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
349
|
-
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
350
|
-
requested: gasLimit,
|
|
351
|
-
capped: maxGas,
|
|
352
|
-
});
|
|
353
|
-
gasLimit = maxGas;
|
|
354
|
-
}
|
|
460
|
+
// Collect earliest txTimeoutAt across all requests.
|
|
461
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
355
462
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
356
|
-
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined;
|
|
357
|
-
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
463
|
+
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined;
|
|
358
464
|
|
|
359
465
|
// Sort the requests so that proposals always go first
|
|
360
466
|
// This ensures the committee gets precomputed correctly
|
|
361
467
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
362
468
|
|
|
363
469
|
try {
|
|
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;
|
|
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 };
|
|
496
|
+
|
|
364
497
|
this.log.debug('Forwarding transactions', {
|
|
365
|
-
|
|
498
|
+
requests: requests.map(request => request.action),
|
|
366
499
|
txConfig,
|
|
367
500
|
});
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
501
|
+
const result = await this.forwardWithPublisherRotation(requests, txConfig, blobConfig);
|
|
502
|
+
if (result === undefined) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(requests, result);
|
|
506
|
+
const allFailedActions = [...failedActions, ...droppedRequests.map(d => d.request.action)];
|
|
507
|
+
return {
|
|
508
|
+
result,
|
|
509
|
+
expiredActions,
|
|
510
|
+
sentActions: requests.map(x => x.action),
|
|
511
|
+
successfulActions,
|
|
512
|
+
failedActions: allFailedActions,
|
|
513
|
+
};
|
|
378
514
|
} catch (err) {
|
|
379
515
|
const viemError = formatViemError(err);
|
|
380
516
|
this.log.error(`Failed to publish bundled transactions`, viemError);
|
|
@@ -391,49 +527,230 @@ export class SequencerPublisher {
|
|
|
391
527
|
}
|
|
392
528
|
}
|
|
393
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
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
566
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
567
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
568
|
+
*/
|
|
569
|
+
private async forwardWithPublisherRotation(
|
|
570
|
+
validRequests: RequestWithExpiry[],
|
|
571
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
572
|
+
blobConfig: L1BlobInputs | undefined,
|
|
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
|
+
|
|
579
|
+
const triedAddresses: EthAddress[] = [];
|
|
580
|
+
let currentPublisher = this.l1TxUtils;
|
|
581
|
+
|
|
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
|
+
}
|
|
589
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
const result = await Multicall3.forward(
|
|
593
|
+
validRequests.map(r => r.request),
|
|
594
|
+
currentPublisher,
|
|
595
|
+
txConfigWithGasLimit,
|
|
596
|
+
blobConfig,
|
|
597
|
+
{ gasLimitRequired: true },
|
|
598
|
+
);
|
|
599
|
+
this.l1TxUtils = currentPublisher;
|
|
600
|
+
return result;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (err instanceof TimeoutError) {
|
|
603
|
+
throw err;
|
|
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
|
+
}
|
|
611
|
+
const viemError = formatViemError(err);
|
|
612
|
+
if (!this.getNextPublisher) {
|
|
613
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
this.log.warn(
|
|
617
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
618
|
+
viemError,
|
|
619
|
+
);
|
|
620
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
621
|
+
if (!nextPublisher) {
|
|
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
|
+
);
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
currentPublisher = nextPublisher;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
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
|
+
|
|
394
701
|
private callbackBundledTransactions(
|
|
395
702
|
requests: RequestWithExpiry[],
|
|
396
|
-
result
|
|
703
|
+
result: { receipt: TransactionReceipt; multicallData: Hex },
|
|
397
704
|
) {
|
|
398
705
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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);
|
|
412
723
|
}
|
|
413
|
-
return { successfulActions, failedActions };
|
|
414
724
|
}
|
|
725
|
+
return { successfulActions, failedActions };
|
|
415
726
|
}
|
|
416
727
|
|
|
417
728
|
/**
|
|
418
|
-
* @notice Will call `
|
|
729
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
419
730
|
* @param tipArchive - The archive to check
|
|
420
731
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
421
732
|
*/
|
|
422
|
-
public
|
|
423
|
-
tipArchive: Fr,
|
|
424
|
-
msgSender: EthAddress,
|
|
425
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
426
|
-
) {
|
|
733
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
427
734
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
428
|
-
|
|
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;
|
|
429
743
|
|
|
430
744
|
return this.rollupContract
|
|
431
|
-
.
|
|
432
|
-
|
|
433
|
-
|
|
745
|
+
.canProposeAt(
|
|
746
|
+
tipArchive.toBuffer(),
|
|
747
|
+
msgSender.toString(),
|
|
748
|
+
nextL1SlotTs,
|
|
749
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
750
|
+
)
|
|
434
751
|
.catch(err => {
|
|
435
|
-
if (err instanceof FormattedViemError &&
|
|
436
|
-
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))}`, {
|
|
437
754
|
error: err.message,
|
|
438
755
|
});
|
|
439
756
|
} else {
|
|
@@ -442,22 +759,23 @@ export class SequencerPublisher {
|
|
|
442
759
|
return undefined;
|
|
443
760
|
});
|
|
444
761
|
}
|
|
762
|
+
|
|
445
763
|
/**
|
|
446
|
-
* @notice Will simulate `
|
|
447
|
-
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header
|
|
448
|
-
* It will throw if the
|
|
449
|
-
* @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
|
|
450
768
|
*/
|
|
451
|
-
@trackSpan('SequencerPublisher.
|
|
452
|
-
public async
|
|
769
|
+
@trackSpan('SequencerPublisher.validateCheckpointHeader')
|
|
770
|
+
public async validateCheckpointHeader(
|
|
453
771
|
header: CheckpointHeader,
|
|
454
|
-
|
|
772
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
455
773
|
): Promise<void> {
|
|
456
774
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
457
775
|
|
|
458
776
|
const args = [
|
|
459
777
|
header.toViem(),
|
|
460
|
-
CommitteeAttestationsAndSigners.
|
|
778
|
+
CommitteeAttestationsAndSigners.packAttestations([]),
|
|
461
779
|
[], // no signers
|
|
462
780
|
Signature.empty().toViemSignature(),
|
|
463
781
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
@@ -465,21 +783,13 @@ export class SequencerPublisher {
|
|
|
465
783
|
flags,
|
|
466
784
|
] as const;
|
|
467
785
|
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
let balance = 0n;
|
|
473
|
-
if (this.config.fishermanMode) {
|
|
474
|
-
// In fisherman mode, we can't know where the proposer is publishing from
|
|
475
|
-
// so we just add sufficient balance to the multicall3 address
|
|
476
|
-
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
477
|
-
} else {
|
|
478
|
-
balance = await this.l1TxUtils.getSenderBalance();
|
|
479
|
-
}
|
|
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.
|
|
480
790
|
stateOverrides.push({
|
|
481
791
|
address: MULTI_CALL_3_ADDRESS,
|
|
482
|
-
balance,
|
|
792
|
+
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
483
793
|
});
|
|
484
794
|
|
|
485
795
|
await this.l1TxUtils.simulate(
|
|
@@ -488,7 +798,7 @@ export class SequencerPublisher {
|
|
|
488
798
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
489
799
|
from: MULTI_CALL_3_ADDRESS,
|
|
490
800
|
},
|
|
491
|
-
{ time: ts
|
|
801
|
+
{ time: ts },
|
|
492
802
|
stateOverrides,
|
|
493
803
|
);
|
|
494
804
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -521,6 +831,8 @@ export class SequencerPublisher {
|
|
|
521
831
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
832
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
523
833
|
|
|
834
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
835
|
+
|
|
524
836
|
try {
|
|
525
837
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
838
|
request,
|
|
@@ -539,6 +851,7 @@ export class SequencerPublisher {
|
|
|
539
851
|
gasUsed,
|
|
540
852
|
checkpointNumber,
|
|
541
853
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
854
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
542
855
|
reason,
|
|
543
856
|
};
|
|
544
857
|
} catch (err) {
|
|
@@ -551,8 +864,8 @@ export class SequencerPublisher {
|
|
|
551
864
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
552
865
|
{ ...logData, request, error: viemError.message },
|
|
553
866
|
);
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
867
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
868
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
556
869
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
557
870
|
return undefined;
|
|
558
871
|
} else {
|
|
@@ -572,6 +885,18 @@ export class SequencerPublisher {
|
|
|
572
885
|
|
|
573
886
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
887
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
888
|
+
this.backupFailedTx({
|
|
889
|
+
id: keccak256(request.data!),
|
|
890
|
+
failureType: 'simulation',
|
|
891
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
892
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
893
|
+
error: { message: viemError.message, name: viemError.name },
|
|
894
|
+
context: {
|
|
895
|
+
actions: [`invalidate-${reason}`],
|
|
896
|
+
checkpointNumber,
|
|
897
|
+
sender: this.getSenderAddress().toString(),
|
|
898
|
+
},
|
|
899
|
+
});
|
|
575
900
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
576
901
|
}
|
|
577
902
|
}
|
|
@@ -585,9 +910,11 @@ export class SequencerPublisher {
|
|
|
585
910
|
const logData = { ...checkpoint, reason };
|
|
586
911
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
587
912
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
)
|
|
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;
|
|
591
918
|
|
|
592
919
|
if (reason === 'invalid-attestation') {
|
|
593
920
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -608,59 +935,11 @@ export class SequencerPublisher {
|
|
|
608
935
|
}
|
|
609
936
|
}
|
|
610
937
|
|
|
611
|
-
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
612
|
-
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
613
|
-
public async validateCheckpointForSubmission(
|
|
614
|
-
checkpoint: Checkpoint,
|
|
615
|
-
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
616
|
-
attestationsAndSignersSignature: Signature,
|
|
617
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
618
|
-
): Promise<bigint> {
|
|
619
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
620
|
-
|
|
621
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
622
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
623
|
-
// so that the committee is recalculated correctly
|
|
624
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
625
|
-
// if (ignoreSignatures) {
|
|
626
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
627
|
-
// if (!committee) {
|
|
628
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
629
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
630
|
-
// }
|
|
631
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
632
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
633
|
-
// );
|
|
634
|
-
// }
|
|
635
|
-
|
|
636
|
-
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
638
|
-
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
|
-
|
|
640
|
-
const args = [
|
|
641
|
-
{
|
|
642
|
-
header: checkpoint.header.toViem(),
|
|
643
|
-
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
|
-
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier: 0n,
|
|
646
|
-
},
|
|
647
|
-
},
|
|
648
|
-
attestationsAndSigners.getPackedAttestations(),
|
|
649
|
-
attestationsAndSigners.getSigners().map(signer => signer.toString()),
|
|
650
|
-
attestationsAndSignersSignature.toViemSignature(),
|
|
651
|
-
blobInput,
|
|
652
|
-
] as const;
|
|
653
|
-
|
|
654
|
-
await this.simulateProposeTx(args, ts, options);
|
|
655
|
-
return ts;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
938
|
private async enqueueCastSignalHelper(
|
|
659
939
|
slotNumber: SlotNumber,
|
|
660
|
-
timestamp: bigint,
|
|
661
940
|
signalType: GovernanceSignalAction,
|
|
662
941
|
payload: EthAddress,
|
|
663
|
-
base:
|
|
942
|
+
base: GovernanceProposerContract,
|
|
664
943
|
signerAddress: EthAddress,
|
|
665
944
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
666
945
|
): Promise<boolean> {
|
|
@@ -675,6 +954,19 @@ export class SequencerPublisher {
|
|
|
675
954
|
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
676
955
|
return false;
|
|
677
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
|
+
|
|
678
970
|
const round = await base.computeRound(slotNumber);
|
|
679
971
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
680
972
|
|
|
@@ -686,11 +978,46 @@ export class SequencerPublisher {
|
|
|
686
978
|
return false;
|
|
687
979
|
}
|
|
688
980
|
|
|
689
|
-
if (await
|
|
981
|
+
if (await base.isPayloadEmpty(payload)) {
|
|
690
982
|
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
691
983
|
return false;
|
|
692
984
|
}
|
|
693
985
|
|
|
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;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
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
|
+
);
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
694
1021
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
1022
|
this.lastActions[signalType] = slotNumber;
|
|
696
1023
|
const action = signalType;
|
|
@@ -709,26 +1036,19 @@ export class SequencerPublisher {
|
|
|
709
1036
|
lastValidL2Slot: slotNumber,
|
|
710
1037
|
});
|
|
711
1038
|
|
|
712
|
-
try {
|
|
713
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
714
|
-
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
715
|
-
} catch (err) {
|
|
716
|
-
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
|
|
717
|
-
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
718
|
-
}
|
|
719
|
-
|
|
720
1039
|
// TODO(palla/slash): All votes (governance and slashing) should txTimeoutAt at the end of the slot.
|
|
721
1040
|
this.addRequest({
|
|
722
|
-
gasConfig: { gasLimit: SequencerPublisher.VOTE_GAS_GUESS },
|
|
723
1041
|
action,
|
|
724
1042
|
request,
|
|
725
1043
|
lastValidL2Slot: slotNumber,
|
|
726
1044
|
checkSuccess: (_request, result) => {
|
|
727
1045
|
const success =
|
|
728
1046
|
result &&
|
|
729
|
-
result.receipt
|
|
730
|
-
|
|
731
|
-
|
|
1047
|
+
extractEventSuccess(result.receipt, {
|
|
1048
|
+
address: base.address.toString(),
|
|
1049
|
+
abi: EmpireBaseAbi,
|
|
1050
|
+
eventName: 'SignalCast',
|
|
1051
|
+
});
|
|
732
1052
|
|
|
733
1053
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
734
1054
|
if (!success) {
|
|
@@ -750,33 +1070,19 @@ export class SequencerPublisher {
|
|
|
750
1070
|
return true;
|
|
751
1071
|
}
|
|
752
1072
|
|
|
753
|
-
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
754
|
-
const key = payload.toString();
|
|
755
|
-
const cached = this.isPayloadEmptyCache.get(key);
|
|
756
|
-
if (cached) {
|
|
757
|
-
return cached;
|
|
758
|
-
}
|
|
759
|
-
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
760
|
-
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
761
|
-
return isEmpty;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
1073
|
/**
|
|
765
1074
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
766
1075
|
* @param slotNumber - The slot number to cast a signal for.
|
|
767
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
768
1076
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
769
1077
|
*/
|
|
770
1078
|
public enqueueGovernanceCastSignal(
|
|
771
1079
|
governancePayload: EthAddress,
|
|
772
1080
|
slotNumber: SlotNumber,
|
|
773
|
-
timestamp: bigint,
|
|
774
1081
|
signerAddress: EthAddress,
|
|
775
1082
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
776
1083
|
): Promise<boolean> {
|
|
777
1084
|
return this.enqueueCastSignalHelper(
|
|
778
1085
|
slotNumber,
|
|
779
|
-
timestamp,
|
|
780
1086
|
'governance-signal',
|
|
781
1087
|
governancePayload,
|
|
782
1088
|
this.govProposerContract,
|
|
@@ -785,11 +1091,51 @@ export class SequencerPublisher {
|
|
|
785
1091
|
);
|
|
786
1092
|
}
|
|
787
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
|
+
|
|
788
1135
|
/** Enqueues all slashing actions as returned by the slasher client. */
|
|
789
1136
|
public async enqueueSlashingActions(
|
|
790
1137
|
actions: ProposerSlashAction[],
|
|
791
1138
|
slotNumber: SlotNumber,
|
|
792
|
-
timestamp: bigint,
|
|
793
1139
|
signerAddress: EthAddress,
|
|
794
1140
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
795
1141
|
): Promise<boolean> {
|
|
@@ -800,58 +1146,6 @@ export class SequencerPublisher {
|
|
|
800
1146
|
|
|
801
1147
|
for (const action of actions) {
|
|
802
1148
|
switch (action.type) {
|
|
803
|
-
case 'vote-empire-payload': {
|
|
804
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
805
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
809
|
-
signerAddress,
|
|
810
|
-
});
|
|
811
|
-
await this.enqueueCastSignalHelper(
|
|
812
|
-
slotNumber,
|
|
813
|
-
timestamp,
|
|
814
|
-
'empire-slashing-signal',
|
|
815
|
-
action.payload,
|
|
816
|
-
this.slashingProposerContract,
|
|
817
|
-
signerAddress,
|
|
818
|
-
signer,
|
|
819
|
-
);
|
|
820
|
-
break;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
case 'create-empire-payload': {
|
|
824
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
825
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
826
|
-
await this.simulateAndEnqueueRequest(
|
|
827
|
-
'create-empire-payload',
|
|
828
|
-
request,
|
|
829
|
-
(receipt: TransactionReceipt) =>
|
|
830
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
831
|
-
slotNumber,
|
|
832
|
-
timestamp,
|
|
833
|
-
);
|
|
834
|
-
break;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
case 'execute-empire-payload': {
|
|
838
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
839
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
840
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
841
|
-
return false;
|
|
842
|
-
}
|
|
843
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
844
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
845
|
-
await this.simulateAndEnqueueRequest(
|
|
846
|
-
'execute-empire-payload',
|
|
847
|
-
request,
|
|
848
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
849
|
-
slotNumber,
|
|
850
|
-
timestamp,
|
|
851
|
-
);
|
|
852
|
-
break;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
1149
|
case 'vote-offenses': {
|
|
856
1150
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
857
1151
|
slotNumber,
|
|
@@ -859,19 +1153,21 @@ export class SequencerPublisher {
|
|
|
859
1153
|
votesCount: action.votes.length,
|
|
860
1154
|
signerAddress,
|
|
861
1155
|
});
|
|
862
|
-
if (this.slashingProposerContract
|
|
863
|
-
this.log.error('
|
|
1156
|
+
if (!this.slashingProposerContract) {
|
|
1157
|
+
this.log.error('No slashing proposer contract available');
|
|
864
1158
|
return false;
|
|
865
1159
|
}
|
|
866
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
867
1160
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
868
|
-
const request = await
|
|
869
|
-
|
|
1161
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
1162
|
+
this.enqueueRequest(
|
|
870
1163
|
'vote-offenses',
|
|
871
1164
|
request,
|
|
872
|
-
|
|
1165
|
+
{
|
|
1166
|
+
address: this.slashingProposerContract.address.toString(),
|
|
1167
|
+
abi: SlashingProposerAbi,
|
|
1168
|
+
eventName: 'VoteCast',
|
|
1169
|
+
},
|
|
873
1170
|
slotNumber,
|
|
874
|
-
timestamp,
|
|
875
1171
|
);
|
|
876
1172
|
break;
|
|
877
1173
|
}
|
|
@@ -882,18 +1178,23 @@ export class SequencerPublisher {
|
|
|
882
1178
|
round: action.round,
|
|
883
1179
|
signerAddress,
|
|
884
1180
|
});
|
|
885
|
-
if (this.slashingProposerContract
|
|
886
|
-
this.log.error('
|
|
1181
|
+
if (!this.slashingProposerContract) {
|
|
1182
|
+
this.log.error('No slashing proposer contract available');
|
|
887
1183
|
return false;
|
|
888
1184
|
}
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
1185
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1186
|
+
action.round,
|
|
1187
|
+
action.committees,
|
|
1188
|
+
);
|
|
1189
|
+
this.enqueueRequest(
|
|
892
1190
|
'execute-slash',
|
|
893
|
-
|
|
894
|
-
|
|
1191
|
+
executeRequest,
|
|
1192
|
+
{
|
|
1193
|
+
address: this.slashingProposerContract.address.toString(),
|
|
1194
|
+
abi: SlashingProposerAbi,
|
|
1195
|
+
eventName: 'RoundExecuted',
|
|
1196
|
+
},
|
|
895
1197
|
slotNumber,
|
|
896
|
-
timestamp,
|
|
897
1198
|
);
|
|
898
1199
|
break;
|
|
899
1200
|
}
|
|
@@ -908,51 +1209,32 @@ export class SequencerPublisher {
|
|
|
908
1209
|
return true;
|
|
909
1210
|
}
|
|
910
1211
|
|
|
911
|
-
/**
|
|
1212
|
+
/** Enqueues a proposal for a checkpoint on L1 */
|
|
912
1213
|
public async enqueueProposeCheckpoint(
|
|
913
1214
|
checkpoint: Checkpoint,
|
|
914
1215
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
915
1216
|
attestationsAndSignersSignature: Signature,
|
|
916
|
-
opts:
|
|
1217
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
917
1218
|
): Promise<void> {
|
|
918
1219
|
const checkpointHeader = checkpoint.header;
|
|
919
1220
|
|
|
920
1221
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1222
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
1223
|
|
|
923
|
-
const proposeTxArgs = {
|
|
1224
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
1225
|
header: checkpointHeader,
|
|
925
1226
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
1227
|
blobs,
|
|
927
1228
|
attestationsAndSigners,
|
|
928
1229
|
attestationsAndSignersSignature,
|
|
1230
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
1231
|
};
|
|
930
1232
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
937
|
-
// make time consistency checks break.
|
|
938
|
-
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
939
|
-
ts = await this.validateCheckpointForSubmission(
|
|
940
|
-
checkpoint,
|
|
941
|
-
attestationsAndSigners,
|
|
942
|
-
attestationsAndSignersSignature,
|
|
943
|
-
opts,
|
|
944
|
-
);
|
|
945
|
-
} catch (err: any) {
|
|
946
|
-
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
947
|
-
...checkpoint.getStats(),
|
|
948
|
-
slotNumber: checkpoint.header.slotNumber,
|
|
949
|
-
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
950
|
-
});
|
|
951
|
-
throw err;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
955
|
-
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 });
|
|
956
1238
|
}
|
|
957
1239
|
|
|
958
1240
|
public enqueueInvalidateCheckpoint(
|
|
@@ -963,23 +1245,22 @@ export class SequencerPublisher {
|
|
|
963
1245
|
return;
|
|
964
1246
|
}
|
|
965
1247
|
|
|
966
|
-
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
967
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
968
|
-
|
|
969
1248
|
const { gasUsed, checkpointNumber } = request;
|
|
970
|
-
const logData = { gasUsed, checkpointNumber,
|
|
1249
|
+
const logData = { gasUsed, checkpointNumber, opts };
|
|
971
1250
|
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
972
1251
|
this.addRequest({
|
|
973
1252
|
action: `invalidate-by-${request.reason}`,
|
|
974
1253
|
request: request.request,
|
|
975
|
-
gasConfig: {
|
|
1254
|
+
gasConfig: opts.txTimeoutAt ? { txTimeoutAt: opts.txTimeoutAt } : undefined,
|
|
976
1255
|
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
977
1256
|
checkSuccess: (_req, result) => {
|
|
978
1257
|
const success =
|
|
979
1258
|
result &&
|
|
980
|
-
result.receipt
|
|
981
|
-
|
|
982
|
-
|
|
1259
|
+
extractEventSuccess(result.receipt, {
|
|
1260
|
+
address: this.rollupContract.address,
|
|
1261
|
+
abi: RollupAbi,
|
|
1262
|
+
eventName: 'CheckpointInvalidated',
|
|
1263
|
+
});
|
|
983
1264
|
if (!success) {
|
|
984
1265
|
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
985
1266
|
} else {
|
|
@@ -990,57 +1271,36 @@ export class SequencerPublisher {
|
|
|
990
1271
|
});
|
|
991
1272
|
}
|
|
992
1273
|
|
|
993
|
-
|
|
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(
|
|
994
1280
|
action: Action,
|
|
995
1281
|
request: L1TxRequest,
|
|
996
|
-
|
|
1282
|
+
eventOpts: { address: string; abi: Abi; eventName: string },
|
|
997
1283
|
slotNumber: SlotNumber,
|
|
998
|
-
|
|
999
|
-
) {
|
|
1000
|
-
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
1284
|
+
): boolean {
|
|
1001
1285
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
1002
1286
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
1003
1287
|
return false;
|
|
1004
1288
|
}
|
|
1005
|
-
|
|
1006
1289
|
const cachedLastActionSlot = this.lastActions[action];
|
|
1007
1290
|
this.lastActions[action] = slotNumber;
|
|
1008
1291
|
|
|
1009
|
-
this.log.debug(`
|
|
1010
|
-
|
|
1011
|
-
let gasUsed: bigint;
|
|
1012
|
-
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1013
|
-
try {
|
|
1014
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
1015
|
-
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1016
|
-
} catch (err) {
|
|
1017
|
-
const viemError = formatViemError(err, simulateAbi);
|
|
1018
|
-
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
|
-
|
|
1020
|
-
return false;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1024
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1025
|
-
logData.gasLimit = gasLimit;
|
|
1026
|
-
|
|
1027
|
-
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1028
|
-
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1029
|
-
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1030
|
-
|
|
1031
|
-
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1292
|
+
this.log.debug(`Enqueuing ${action}`, { slotNumber });
|
|
1032
1293
|
this.addRequest({
|
|
1033
1294
|
action,
|
|
1034
|
-
request
|
|
1035
|
-
gasConfig: { gasLimit },
|
|
1295
|
+
request,
|
|
1036
1296
|
lastValidL2Slot: slotNumber,
|
|
1037
|
-
checkSuccess: (
|
|
1038
|
-
const success = result && result.receipt
|
|
1297
|
+
checkSuccess: (_request, result) => {
|
|
1298
|
+
const success = result && extractEventSuccess(result.receipt, eventOpts);
|
|
1039
1299
|
if (!success) {
|
|
1040
|
-
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result,
|
|
1300
|
+
this.log.warn(`Action ${action} at ${slotNumber} failed`, { ...result, slotNumber });
|
|
1041
1301
|
this.lastActions[action] = cachedLastActionSlot;
|
|
1042
1302
|
} else {
|
|
1043
|
-
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result,
|
|
1303
|
+
this.log.info(`Action ${action} at ${slotNumber} succeeded`, { ...result, slotNumber });
|
|
1044
1304
|
}
|
|
1045
1305
|
return !!success;
|
|
1046
1306
|
},
|
|
@@ -1056,6 +1316,7 @@ export class SequencerPublisher {
|
|
|
1056
1316
|
*/
|
|
1057
1317
|
public interrupt() {
|
|
1058
1318
|
this.interrupted = true;
|
|
1319
|
+
this.interruptibleSleep.interrupt();
|
|
1059
1320
|
this.l1TxUtils.interrupt();
|
|
1060
1321
|
}
|
|
1061
1322
|
|
|
@@ -1065,11 +1326,7 @@ export class SequencerPublisher {
|
|
|
1065
1326
|
this.l1TxUtils.restart();
|
|
1066
1327
|
}
|
|
1067
1328
|
|
|
1068
|
-
private async prepareProposeTx(
|
|
1069
|
-
encodedData: L1ProcessArgs,
|
|
1070
|
-
timestamp: bigint,
|
|
1071
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1072
|
-
) {
|
|
1329
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs) {
|
|
1073
1330
|
const kzg = Blob.getViemKzgInstance();
|
|
1074
1331
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1075
1332
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1082,7 +1339,11 @@ export class SequencerPublisher {
|
|
|
1082
1339
|
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
1083
1340
|
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
1084
1341
|
} else {
|
|
1085
|
-
//
|
|
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.
|
|
1086
1347
|
blobEvaluationGas = await this.l1TxUtils
|
|
1087
1348
|
.estimateGas(
|
|
1088
1349
|
this.getSenderAddress().toString(),
|
|
@@ -1100,9 +1361,27 @@ export class SequencerPublisher {
|
|
|
1100
1361
|
kzg,
|
|
1101
1362
|
},
|
|
1102
1363
|
)
|
|
1103
|
-
.catch(err => {
|
|
1104
|
-
const
|
|
1105
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1364
|
+
.catch(async err => {
|
|
1365
|
+
const viemError = formatViemError(err);
|
|
1366
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1367
|
+
const validateBlobsData = encodeFunctionData({
|
|
1368
|
+
abi: RollupAbi,
|
|
1369
|
+
functionName: 'validateBlobs',
|
|
1370
|
+
args: [blobInput],
|
|
1371
|
+
});
|
|
1372
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1373
|
+
this.backupFailedTx({
|
|
1374
|
+
id: keccak256(validateBlobsData),
|
|
1375
|
+
failureType: 'simulation',
|
|
1376
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1377
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1378
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1379
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1380
|
+
context: {
|
|
1381
|
+
actions: ['validate-blobs'],
|
|
1382
|
+
sender: this.getSenderAddress().toString(),
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1106
1385
|
throw new Error('Failed to validate blobs');
|
|
1107
1386
|
});
|
|
1108
1387
|
}
|
|
@@ -1113,8 +1392,7 @@ export class SequencerPublisher {
|
|
|
1113
1392
|
header: encodedData.header.toViem(),
|
|
1114
1393
|
archive: toHex(encodedData.archive),
|
|
1115
1394
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1395
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1396
|
},
|
|
1119
1397
|
},
|
|
1120
1398
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1123,124 +1401,21 @@ export class SequencerPublisher {
|
|
|
1123
1401
|
blobInput,
|
|
1124
1402
|
] as const;
|
|
1125
1403
|
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Simulates the propose tx with eth_simulateV1
|
|
1133
|
-
* @param args - The propose tx args
|
|
1134
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1135
|
-
* @returns The simulation result
|
|
1136
|
-
*/
|
|
1137
|
-
private async simulateProposeTx(
|
|
1138
|
-
args: readonly [
|
|
1139
|
-
{
|
|
1140
|
-
readonly header: ViemHeader;
|
|
1141
|
-
readonly archive: `0x${string}`;
|
|
1142
|
-
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier: 0n;
|
|
1144
|
-
};
|
|
1145
|
-
},
|
|
1146
|
-
ViemCommitteeAttestations,
|
|
1147
|
-
`0x${string}`[], // Signers
|
|
1148
|
-
ViemSignature,
|
|
1149
|
-
`0x${string}`,
|
|
1150
|
-
],
|
|
1151
|
-
timestamp: bigint,
|
|
1152
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1153
|
-
) {
|
|
1154
|
-
const rollupData = encodeFunctionData({
|
|
1155
|
-
abi: RollupAbi,
|
|
1156
|
-
functionName: 'propose',
|
|
1157
|
-
args,
|
|
1158
|
-
});
|
|
1159
|
-
|
|
1160
|
-
// override the pending checkpoint number if requested
|
|
1161
|
-
const forcePendingCheckpointNumberStateDiff = (
|
|
1162
|
-
options.forcePendingCheckpointNumber !== undefined
|
|
1163
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1164
|
-
: []
|
|
1165
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
1166
|
-
|
|
1167
|
-
const stateOverrides: StateOverride = [
|
|
1168
|
-
{
|
|
1169
|
-
address: this.rollupContract.address,
|
|
1170
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1171
|
-
stateDiff: [
|
|
1172
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1173
|
-
...forcePendingCheckpointNumberStateDiff,
|
|
1174
|
-
],
|
|
1175
|
-
},
|
|
1176
|
-
];
|
|
1177
|
-
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1178
|
-
if (this.proposerAddressForSimulation) {
|
|
1179
|
-
stateOverrides.push({
|
|
1180
|
-
address: this.proposerAddressForSimulation.toString(),
|
|
1181
|
-
balance: 10n * WEI_CONST * WEI_CONST, // 10 ETH
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
const simulationResult = await this.l1TxUtils
|
|
1186
|
-
.simulate(
|
|
1187
|
-
{
|
|
1188
|
-
to: this.rollupContract.address,
|
|
1189
|
-
data: rollupData,
|
|
1190
|
-
gas: MAX_L1_TX_LIMIT,
|
|
1191
|
-
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1192
|
-
},
|
|
1193
|
-
{
|
|
1194
|
-
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1195
|
-
time: timestamp + 1n,
|
|
1196
|
-
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1197
|
-
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1198
|
-
},
|
|
1199
|
-
stateOverrides,
|
|
1200
|
-
RollupAbi,
|
|
1201
|
-
{
|
|
1202
|
-
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1203
|
-
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1204
|
-
},
|
|
1205
|
-
)
|
|
1206
|
-
.catch(err => {
|
|
1207
|
-
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
1208
|
-
const viemError = formatViemError(err);
|
|
1209
|
-
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
1210
|
-
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1211
|
-
// Return a minimal simulation result with the fallback gas estimate
|
|
1212
|
-
return {
|
|
1213
|
-
gasUsed: MAX_L1_TX_LIMIT,
|
|
1214
|
-
logs: [],
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1218
|
-
throw err;
|
|
1219
|
-
});
|
|
1404
|
+
const rollupData = encodeFunctionData({ abi: RollupAbi, functionName: 'propose', args });
|
|
1220
1405
|
|
|
1221
|
-
return {
|
|
1406
|
+
return { args, blobEvaluationGas, rollupData };
|
|
1222
1407
|
}
|
|
1223
1408
|
|
|
1224
1409
|
private async addProposeTx(
|
|
1225
1410
|
checkpoint: Checkpoint,
|
|
1226
1411
|
encodedData: L1ProcessArgs,
|
|
1227
|
-
opts:
|
|
1228
|
-
timestamp: bigint,
|
|
1412
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1229
1413
|
): Promise<void> {
|
|
1230
1414
|
const slot = checkpoint.header.slotNumber;
|
|
1231
1415
|
const timer = new Timer();
|
|
1232
1416
|
const kzg = Blob.getViemKzgInstance();
|
|
1233
|
-
const { rollupData,
|
|
1234
|
-
encodedData,
|
|
1235
|
-
timestamp,
|
|
1236
|
-
opts,
|
|
1237
|
-
);
|
|
1417
|
+
const { rollupData, blobEvaluationGas } = await this.prepareProposeTx(encodedData);
|
|
1238
1418
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1239
|
-
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
1240
|
-
BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
|
|
1241
|
-
blobEvaluationGas +
|
|
1242
|
-
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1243
|
-
);
|
|
1244
1419
|
|
|
1245
1420
|
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1246
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.
|
|
@@ -1257,7 +1432,8 @@ export class SequencerPublisher {
|
|
|
1257
1432
|
data: rollupData,
|
|
1258
1433
|
},
|
|
1259
1434
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1260
|
-
gasConfig: {
|
|
1435
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit: undefined },
|
|
1436
|
+
blobEvaluationGas,
|
|
1261
1437
|
blobConfig: {
|
|
1262
1438
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1263
1439
|
kzg,
|
|
@@ -1267,10 +1443,11 @@ export class SequencerPublisher {
|
|
|
1267
1443
|
return false;
|
|
1268
1444
|
}
|
|
1269
1445
|
const { receipt, stats, errorMsg } = result;
|
|
1270
|
-
const success =
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1446
|
+
const success = extractEventSuccess(receipt, {
|
|
1447
|
+
address: this.rollupContract.address,
|
|
1448
|
+
abi: RollupAbi,
|
|
1449
|
+
eventName: 'CheckpointProposed',
|
|
1450
|
+
});
|
|
1274
1451
|
|
|
1275
1452
|
if (success) {
|
|
1276
1453
|
const endBlock = receipt.blockNumber;
|
|
@@ -1310,4 +1487,10 @@ export class SequencerPublisher {
|
|
|
1310
1487
|
},
|
|
1311
1488
|
});
|
|
1312
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
|
+
}
|
|
1313
1496
|
}
|