@aztec/sequencer-client 0.0.1-commit.29c6b1a3 → 0.0.1-commit.2c0ee1788
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +17 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +63 -30
- package/dest/config.d.ts +27 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +58 -37
- 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 +128 -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 +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +15 -16
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +16 -50
- 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 +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +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-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-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +75 -49
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +472 -158
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +73 -14
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +757 -249
- 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/events.d.ts +7 -1
- 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 +142 -36
- package/dest/sequencer/sequencer.d.ts +55 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +190 -99
- package/dest/sequencer/timetable.d.ts +17 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -46
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +12 -12
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -36
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +10 -8
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +81 -30
- package/src/config.ts +77 -46
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +27 -63
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +157 -45
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +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-publisher-factory.ts +38 -9
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +533 -218
- package/src/sequencer/README.md +82 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +948 -286
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +6 -1
- package/src/sequencer/metrics.ts +164 -39
- package/src/sequencer/sequencer.ts +267 -114
- package/src/sequencer/timetable.ts +70 -57
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +65 -53
- package/src/test/utils.ts +31 -10
|
@@ -3,50 +3,80 @@ 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
8
|
type IEmpireBase,
|
|
9
9
|
MULTI_CALL_3_ADDRESS,
|
|
10
10
|
Multicall3,
|
|
11
11
|
RollupContract,
|
|
12
|
-
|
|
12
|
+
SimulationOverridesBuilder,
|
|
13
|
+
type SimulationOverridesPlan,
|
|
14
|
+
type SlashingProposerContract,
|
|
13
15
|
type ViemCommitteeAttestations,
|
|
14
16
|
type ViemHeader,
|
|
17
|
+
buildSimulationOverridesStateOverride,
|
|
15
18
|
} from '@aztec/ethereum/contracts';
|
|
16
19
|
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
17
20
|
import {
|
|
18
21
|
type L1BlobInputs,
|
|
19
22
|
type L1TxConfig,
|
|
20
23
|
type L1TxRequest,
|
|
24
|
+
type L1TxUtils,
|
|
25
|
+
MAX_L1_TX_LIMIT,
|
|
21
26
|
type TransactionStats,
|
|
22
27
|
WEI_CONST,
|
|
23
28
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
24
|
-
import
|
|
25
|
-
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
29
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
30
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
|
-
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
28
31
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
32
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
29
33
|
import { pick } from '@aztec/foundation/collection';
|
|
30
34
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
31
36
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
32
37
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
33
38
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
39
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
34
41
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
35
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
42
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
36
43
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
37
44
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
38
45
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
39
46
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
40
|
-
import {
|
|
47
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
41
48
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
42
49
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
43
50
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
44
51
|
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
import {
|
|
53
|
+
type Hex,
|
|
54
|
+
type TransactionReceipt,
|
|
55
|
+
type TypedDataDefinition,
|
|
56
|
+
encodeFunctionData,
|
|
57
|
+
keccak256,
|
|
58
|
+
multicall3Abi,
|
|
59
|
+
toHex,
|
|
60
|
+
} from 'viem';
|
|
61
|
+
|
|
62
|
+
import type { SequencerPublisherConfig } from './config.js';
|
|
63
|
+
import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
|
|
48
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
49
65
|
|
|
66
|
+
/** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
|
|
67
|
+
export type SendRequestsResult = {
|
|
68
|
+
/** The L1 transaction receipt or error from the bundled multicall. */
|
|
69
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
|
|
70
|
+
/** Actions that expired (past their deadline) before the request was sent. */
|
|
71
|
+
expiredActions: Action[];
|
|
72
|
+
/** Actions that were included in the sent L1 transaction. */
|
|
73
|
+
sentActions: Action[];
|
|
74
|
+
/** Actions whose L1 simulation succeeded (subset of sentActions). */
|
|
75
|
+
successfulActions: Action[];
|
|
76
|
+
/** Actions whose L1 simulation failed (subset of sentActions). */
|
|
77
|
+
failedActions: Action[];
|
|
78
|
+
};
|
|
79
|
+
|
|
50
80
|
/** Arguments to the process method of the rollup contract */
|
|
51
81
|
type L1ProcessArgs = {
|
|
52
82
|
/** The L2 block header. */
|
|
@@ -59,6 +89,8 @@ type L1ProcessArgs = {
|
|
|
59
89
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
60
90
|
/** Attestations and signers signature */
|
|
61
91
|
attestationsAndSignersSignature: Signature;
|
|
92
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
93
|
+
feeAssetPriceModifier: bigint;
|
|
62
94
|
};
|
|
63
95
|
|
|
64
96
|
export const Actions = [
|
|
@@ -66,16 +98,13 @@ export const Actions = [
|
|
|
66
98
|
'invalidate-by-insufficient-attestations',
|
|
67
99
|
'propose',
|
|
68
100
|
'governance-signal',
|
|
69
|
-
'empire-slashing-signal',
|
|
70
|
-
'create-empire-payload',
|
|
71
|
-
'execute-empire-payload',
|
|
72
101
|
'vote-offenses',
|
|
73
102
|
'execute-slash',
|
|
74
103
|
] as const;
|
|
75
104
|
|
|
76
105
|
export type Action = (typeof Actions)[number];
|
|
77
106
|
|
|
78
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
107
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
79
108
|
|
|
80
109
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
81
110
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -86,6 +115,13 @@ export type InvalidateCheckpointRequest = {
|
|
|
86
115
|
gasUsed: bigint;
|
|
87
116
|
checkpointNumber: CheckpointNumber;
|
|
88
117
|
forcePendingCheckpointNumber: CheckpointNumber;
|
|
118
|
+
/** Archive at the rollback target checkpoint (checkpoint N-1). */
|
|
119
|
+
lastArchive: Fr;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type EnqueueProposeCheckpointOpts = {
|
|
123
|
+
txTimeoutAt?: Date;
|
|
124
|
+
simulationOverridesPlan?: SimulationOverridesPlan;
|
|
89
125
|
};
|
|
90
126
|
|
|
91
127
|
interface RequestWithExpiry {
|
|
@@ -94,6 +130,8 @@ interface RequestWithExpiry {
|
|
|
94
130
|
lastValidL2Slot: SlotNumber;
|
|
95
131
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
96
132
|
blobConfig?: L1BlobInputs;
|
|
133
|
+
/** Optional pre-send validation. If it rejects, the request is discarded. */
|
|
134
|
+
preCheck?: () => Promise<void>;
|
|
97
135
|
checkSuccess: (
|
|
98
136
|
request: L1TxRequest,
|
|
99
137
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -104,6 +142,7 @@ export class SequencerPublisher {
|
|
|
104
142
|
private interrupted = false;
|
|
105
143
|
private metrics: SequencerPublisherMetrics;
|
|
106
144
|
public epochCache: EpochCache;
|
|
145
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
107
146
|
|
|
108
147
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
109
148
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -111,21 +150,31 @@ export class SequencerPublisher {
|
|
|
111
150
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
112
151
|
|
|
113
152
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
153
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
114
154
|
|
|
115
155
|
protected log: Logger;
|
|
116
156
|
protected ethereumSlotDuration: bigint;
|
|
157
|
+
protected aztecSlotDuration: bigint;
|
|
158
|
+
|
|
159
|
+
/** Date provider for wall-clock time. */
|
|
160
|
+
private readonly dateProvider: DateProvider;
|
|
117
161
|
|
|
118
162
|
private blobClient: BlobClientInterface;
|
|
119
163
|
|
|
120
164
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
121
165
|
private proposerAddressForSimulation?: EthAddress;
|
|
122
166
|
|
|
167
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
168
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
169
|
+
|
|
123
170
|
/** L1 fee analyzer for fisherman mode */
|
|
124
171
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
172
|
+
|
|
173
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
174
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
175
|
+
|
|
176
|
+
/** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
|
|
177
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
129
178
|
|
|
130
179
|
// A CALL to a cold address is 2700 gas
|
|
131
180
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -133,44 +182,48 @@ export class SequencerPublisher {
|
|
|
133
182
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
134
183
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
135
184
|
|
|
136
|
-
public l1TxUtils:
|
|
185
|
+
public l1TxUtils: L1TxUtils;
|
|
137
186
|
public rollupContract: RollupContract;
|
|
138
187
|
public govProposerContract: GovernanceProposerContract;
|
|
139
|
-
public slashingProposerContract:
|
|
140
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
188
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
141
189
|
|
|
142
190
|
public readonly tracer: Tracer;
|
|
143
191
|
|
|
144
192
|
protected requests: RequestWithExpiry[] = [];
|
|
145
193
|
|
|
146
194
|
constructor(
|
|
147
|
-
private config:
|
|
195
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
196
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
148
197
|
deps: {
|
|
149
198
|
telemetry?: TelemetryClient;
|
|
150
199
|
blobClient: BlobClientInterface;
|
|
151
|
-
l1TxUtils:
|
|
200
|
+
l1TxUtils: L1TxUtils;
|
|
152
201
|
rollupContract: RollupContract;
|
|
153
|
-
slashingProposerContract:
|
|
202
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
154
203
|
governanceProposerContract: GovernanceProposerContract;
|
|
155
|
-
slashFactoryContract: SlashFactoryContract;
|
|
156
204
|
epochCache: EpochCache;
|
|
157
205
|
dateProvider: DateProvider;
|
|
158
206
|
metrics: SequencerPublisherMetrics;
|
|
159
207
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
160
208
|
log?: Logger;
|
|
209
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
161
210
|
},
|
|
162
211
|
) {
|
|
163
212
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
164
213
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
214
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
215
|
+
this.dateProvider = deps.dateProvider;
|
|
165
216
|
this.epochCache = deps.epochCache;
|
|
166
217
|
this.lastActions = deps.lastActions;
|
|
167
218
|
|
|
168
219
|
this.blobClient = deps.blobClient;
|
|
220
|
+
this.dateProvider = deps.dateProvider;
|
|
169
221
|
|
|
170
222
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
171
223
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
172
224
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
173
225
|
this.l1TxUtils = deps.l1TxUtils;
|
|
226
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
174
227
|
|
|
175
228
|
this.rollupContract = deps.rollupContract;
|
|
176
229
|
|
|
@@ -182,8 +235,6 @@ export class SequencerPublisher {
|
|
|
182
235
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
183
236
|
this.slashingProposerContract = newSlashingProposer;
|
|
184
237
|
});
|
|
185
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
186
|
-
|
|
187
238
|
// Initialize L1 fee analyzer for fisherman mode
|
|
188
239
|
if (config.fishermanMode) {
|
|
189
240
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
@@ -192,12 +243,52 @@ export class SequencerPublisher {
|
|
|
192
243
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
193
244
|
);
|
|
194
245
|
}
|
|
246
|
+
|
|
247
|
+
// Initialize fee asset price oracle
|
|
248
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
249
|
+
this.l1TxUtils.client,
|
|
250
|
+
this.rollupContract,
|
|
251
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
255
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
260
|
+
* Does nothing if no store is configured.
|
|
261
|
+
*/
|
|
262
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
263
|
+
if (!this.failedTxStore) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tx: FailedL1Tx = {
|
|
268
|
+
...failedTx,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Fire and forget - don't block on backup
|
|
273
|
+
void this.failedTxStore
|
|
274
|
+
.then(store => store?.saveFailedTx(tx))
|
|
275
|
+
.catch(err => {
|
|
276
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
277
|
+
});
|
|
195
278
|
}
|
|
196
279
|
|
|
197
280
|
public getRollupContract(): RollupContract {
|
|
198
281
|
return this.rollupContract;
|
|
199
282
|
}
|
|
200
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Gets the fee asset price modifier from the oracle.
|
|
286
|
+
* Returns 0n if the oracle query fails.
|
|
287
|
+
*/
|
|
288
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
289
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
290
|
+
}
|
|
291
|
+
|
|
201
292
|
public getSenderAddress() {
|
|
202
293
|
return this.l1TxUtils.getSenderAddress();
|
|
203
294
|
}
|
|
@@ -222,7 +313,7 @@ export class SequencerPublisher {
|
|
|
222
313
|
}
|
|
223
314
|
|
|
224
315
|
public getCurrentL2Slot(): SlotNumber {
|
|
225
|
-
return this.epochCache.
|
|
316
|
+
return this.epochCache.getSlotNow();
|
|
226
317
|
}
|
|
227
318
|
|
|
228
319
|
/**
|
|
@@ -273,7 +364,7 @@ export class SequencerPublisher {
|
|
|
273
364
|
// Start the analysis
|
|
274
365
|
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
275
366
|
l2SlotNumber,
|
|
276
|
-
gasLimit > 0n ? gasLimit :
|
|
367
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
277
368
|
l1Requests,
|
|
278
369
|
blobConfig,
|
|
279
370
|
onComplete,
|
|
@@ -300,9 +391,10 @@ export class SequencerPublisher {
|
|
|
300
391
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
301
392
|
*/
|
|
302
393
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
303
|
-
public async sendRequests() {
|
|
394
|
+
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
304
395
|
const requestsToProcess = [...this.requests];
|
|
305
396
|
this.requests = [];
|
|
397
|
+
|
|
306
398
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
307
399
|
return undefined;
|
|
308
400
|
}
|
|
@@ -335,8 +427,8 @@ export class SequencerPublisher {
|
|
|
335
427
|
// @note - we can only have one blob config per bundle
|
|
336
428
|
// find requests with gas and blob configs
|
|
337
429
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
338
|
-
const gasConfigs =
|
|
339
|
-
const blobConfigs =
|
|
430
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
431
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
340
432
|
|
|
341
433
|
if (blobConfigs.length > 1) {
|
|
342
434
|
throw new Error('Multiple blob configs found');
|
|
@@ -346,7 +438,16 @@ export class SequencerPublisher {
|
|
|
346
438
|
|
|
347
439
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
348
440
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
349
|
-
|
|
441
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
442
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
443
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
444
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
445
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
446
|
+
requested: gasLimit,
|
|
447
|
+
capped: maxGas,
|
|
448
|
+
});
|
|
449
|
+
gasLimit = maxGas;
|
|
450
|
+
}
|
|
350
451
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
351
452
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
352
453
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -356,19 +457,36 @@ export class SequencerPublisher {
|
|
|
356
457
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
357
458
|
|
|
358
459
|
try {
|
|
460
|
+
// Capture context for failed tx backup before sending
|
|
461
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
462
|
+
const multicallData = encodeFunctionData({
|
|
463
|
+
abi: multicall3Abi,
|
|
464
|
+
functionName: 'aggregate3',
|
|
465
|
+
args: [
|
|
466
|
+
validRequests.map(r => ({
|
|
467
|
+
target: r.request.to!,
|
|
468
|
+
callData: r.request.data!,
|
|
469
|
+
allowFailure: true,
|
|
470
|
+
})),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
474
|
+
|
|
475
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
476
|
+
|
|
359
477
|
this.log.debug('Forwarding transactions', {
|
|
360
478
|
validRequests: validRequests.map(request => request.action),
|
|
361
479
|
txConfig,
|
|
362
480
|
});
|
|
363
|
-
const result = await
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
481
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
482
|
+
if (result === undefined) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
486
|
+
validRequests,
|
|
487
|
+
result,
|
|
488
|
+
txContext,
|
|
370
489
|
);
|
|
371
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
372
490
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
373
491
|
} catch (err) {
|
|
374
492
|
const viemError = formatViemError(err);
|
|
@@ -386,16 +504,127 @@ export class SequencerPublisher {
|
|
|
386
504
|
}
|
|
387
505
|
}
|
|
388
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
509
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
510
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
511
|
+
*/
|
|
512
|
+
private async forwardWithPublisherRotation(
|
|
513
|
+
validRequests: RequestWithExpiry[],
|
|
514
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
515
|
+
blobConfig: L1BlobInputs | undefined,
|
|
516
|
+
) {
|
|
517
|
+
const triedAddresses: EthAddress[] = [];
|
|
518
|
+
let currentPublisher = this.l1TxUtils;
|
|
519
|
+
|
|
520
|
+
while (true) {
|
|
521
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
522
|
+
try {
|
|
523
|
+
const result = await Multicall3.forward(
|
|
524
|
+
validRequests.map(r => r.request),
|
|
525
|
+
currentPublisher,
|
|
526
|
+
txConfig,
|
|
527
|
+
blobConfig,
|
|
528
|
+
this.rollupContract.address,
|
|
529
|
+
this.log,
|
|
530
|
+
);
|
|
531
|
+
this.l1TxUtils = currentPublisher;
|
|
532
|
+
return result;
|
|
533
|
+
} catch (err) {
|
|
534
|
+
if (err instanceof TimeoutError) {
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
537
|
+
const viemError = formatViemError(err);
|
|
538
|
+
if (!this.getNextPublisher) {
|
|
539
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
this.log.warn(
|
|
543
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
544
|
+
viemError,
|
|
545
|
+
);
|
|
546
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
547
|
+
if (!nextPublisher) {
|
|
548
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
currentPublisher = nextPublisher;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/*
|
|
557
|
+
* Schedules sending all enqueued requests at (or after) the given timestamp.
|
|
558
|
+
* Uses InterruptibleSleep so it can be cancelled via interrupt().
|
|
559
|
+
* Returns the promise for the L1 response (caller should NOT await this in the work loop).
|
|
560
|
+
*/
|
|
561
|
+
public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
|
|
562
|
+
const ms = submitAfter.getTime() - this.dateProvider.now();
|
|
563
|
+
if (ms > 0) {
|
|
564
|
+
this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
|
|
565
|
+
await this.interruptibleSleep.sleep(ms);
|
|
566
|
+
}
|
|
567
|
+
if (this.interrupted) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
|
|
572
|
+
const validRequests: RequestWithExpiry[] = [];
|
|
573
|
+
for (const request of this.requests) {
|
|
574
|
+
if (!request.preCheck) {
|
|
575
|
+
validRequests.push(request);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
await request.preCheck();
|
|
581
|
+
validRequests.push(request);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.requests = validRequests;
|
|
588
|
+
if (this.requests.length === 0) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return this.sendRequests();
|
|
593
|
+
}
|
|
594
|
+
|
|
389
595
|
private callbackBundledTransactions(
|
|
390
596
|
requests: RequestWithExpiry[],
|
|
391
|
-
result
|
|
597
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
598
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
392
599
|
) {
|
|
393
600
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
394
601
|
if (result instanceof FormattedViemError) {
|
|
395
602
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
603
|
+
this.backupFailedTx({
|
|
604
|
+
id: keccak256(txContext.multicallData),
|
|
605
|
+
failureType: 'send-error',
|
|
606
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
607
|
+
blobData: txContext.blobData,
|
|
608
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
609
|
+
error: { message: result.message, name: result.name },
|
|
610
|
+
context: {
|
|
611
|
+
actions: requests.map(r => r.action),
|
|
612
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
613
|
+
sender: this.getSenderAddress().toString(),
|
|
614
|
+
},
|
|
615
|
+
});
|
|
396
616
|
return { failedActions: requests.map(r => r.action) };
|
|
397
617
|
} else {
|
|
398
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
618
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
619
|
+
result,
|
|
620
|
+
requests: requests.map(r => ({
|
|
621
|
+
...r,
|
|
622
|
+
// Avoid logging large blob data
|
|
623
|
+
blobConfig: r.blobConfig
|
|
624
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
625
|
+
: undefined,
|
|
626
|
+
})),
|
|
627
|
+
});
|
|
399
628
|
const successfulActions: Action[] = [];
|
|
400
629
|
const failedActions: Action[] = [];
|
|
401
630
|
for (const request of requests) {
|
|
@@ -405,27 +634,54 @@ export class SequencerPublisher {
|
|
|
405
634
|
failedActions.push(request.action);
|
|
406
635
|
}
|
|
407
636
|
}
|
|
637
|
+
// Single backup for the whole reverted tx
|
|
638
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
639
|
+
this.backupFailedTx({
|
|
640
|
+
id: result.receipt.transactionHash,
|
|
641
|
+
failureType: 'revert',
|
|
642
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
643
|
+
blobData: txContext.blobData,
|
|
644
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
645
|
+
receipt: {
|
|
646
|
+
transactionHash: result.receipt.transactionHash,
|
|
647
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
648
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
649
|
+
status: 'reverted',
|
|
650
|
+
},
|
|
651
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
652
|
+
context: {
|
|
653
|
+
actions: failedActions,
|
|
654
|
+
requests: requests
|
|
655
|
+
.filter(r => failedActions.includes(r.action))
|
|
656
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
657
|
+
sender: this.getSenderAddress().toString(),
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
408
661
|
return { successfulActions, failedActions };
|
|
409
662
|
}
|
|
410
663
|
}
|
|
411
664
|
|
|
412
665
|
/**
|
|
413
|
-
* @notice Will call `
|
|
666
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
414
667
|
* @param tipArchive - The archive to check
|
|
415
668
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
416
669
|
*/
|
|
417
|
-
public
|
|
418
|
-
tipArchive: Fr,
|
|
419
|
-
msgSender: EthAddress,
|
|
420
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
421
|
-
) {
|
|
670
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
422
671
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
423
672
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
424
673
|
|
|
674
|
+
const pipelined = this.epochCache.isProposerPipeliningEnabled();
|
|
675
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
676
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
677
|
+
|
|
425
678
|
return this.rollupContract
|
|
426
|
-
.
|
|
427
|
-
|
|
428
|
-
|
|
679
|
+
.canProposeAt(
|
|
680
|
+
tipArchive.toBuffer(),
|
|
681
|
+
msgSender.toString(),
|
|
682
|
+
nextL1SlotTs,
|
|
683
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
684
|
+
)
|
|
429
685
|
.catch(err => {
|
|
430
686
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
431
687
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -437,6 +693,7 @@ export class SequencerPublisher {
|
|
|
437
693
|
return undefined;
|
|
438
694
|
});
|
|
439
695
|
}
|
|
696
|
+
|
|
440
697
|
/**
|
|
441
698
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
442
699
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
@@ -446,13 +703,13 @@ export class SequencerPublisher {
|
|
|
446
703
|
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
447
704
|
public async validateBlockHeader(
|
|
448
705
|
header: CheckpointHeader,
|
|
449
|
-
|
|
706
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
450
707
|
): Promise<void> {
|
|
451
708
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
452
709
|
|
|
453
710
|
const args = [
|
|
454
711
|
header.toViem(),
|
|
455
|
-
CommitteeAttestationsAndSigners.
|
|
712
|
+
CommitteeAttestationsAndSigners.packAttestations([]),
|
|
456
713
|
[], // no signers
|
|
457
714
|
Signature.empty().toViemSignature(),
|
|
458
715
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
@@ -460,10 +717,8 @@ export class SequencerPublisher {
|
|
|
460
717
|
flags,
|
|
461
718
|
] as const;
|
|
462
719
|
|
|
463
|
-
const ts =
|
|
464
|
-
const stateOverrides = await this.rollupContract
|
|
465
|
-
opts?.forcePendingCheckpointNumber,
|
|
466
|
-
);
|
|
720
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
721
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
467
722
|
let balance = 0n;
|
|
468
723
|
if (this.config.fishermanMode) {
|
|
469
724
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -483,7 +738,7 @@ export class SequencerPublisher {
|
|
|
483
738
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
484
739
|
from: MULTI_CALL_3_ADDRESS,
|
|
485
740
|
},
|
|
486
|
-
{ time: ts
|
|
741
|
+
{ time: ts },
|
|
487
742
|
stateOverrides,
|
|
488
743
|
);
|
|
489
744
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -516,8 +771,15 @@ export class SequencerPublisher {
|
|
|
516
771
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
517
772
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
518
773
|
|
|
774
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
775
|
+
|
|
519
776
|
try {
|
|
520
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
777
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
778
|
+
request,
|
|
779
|
+
undefined,
|
|
780
|
+
undefined,
|
|
781
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
782
|
+
);
|
|
521
783
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
522
784
|
...logData,
|
|
523
785
|
request,
|
|
@@ -529,6 +791,7 @@ export class SequencerPublisher {
|
|
|
529
791
|
gasUsed,
|
|
530
792
|
checkpointNumber,
|
|
531
793
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
794
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
532
795
|
reason,
|
|
533
796
|
};
|
|
534
797
|
} catch (err) {
|
|
@@ -536,13 +799,13 @@ export class SequencerPublisher {
|
|
|
536
799
|
|
|
537
800
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
538
801
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
539
|
-
if (viemError.message?.includes('
|
|
802
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
540
803
|
this.log.verbose(
|
|
541
804
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
542
805
|
{ ...logData, request, error: viemError.message },
|
|
543
806
|
);
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
807
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
808
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
546
809
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
547
810
|
return undefined;
|
|
548
811
|
} else {
|
|
@@ -562,6 +825,18 @@ export class SequencerPublisher {
|
|
|
562
825
|
|
|
563
826
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
564
827
|
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
828
|
+
this.backupFailedTx({
|
|
829
|
+
id: keccak256(request.data!),
|
|
830
|
+
failureType: 'simulation',
|
|
831
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
832
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
833
|
+
error: { message: viemError.message, name: viemError.name },
|
|
834
|
+
context: {
|
|
835
|
+
actions: [`invalidate-${reason}`],
|
|
836
|
+
checkpointNumber,
|
|
837
|
+
sender: this.getSenderAddress().toString(),
|
|
838
|
+
},
|
|
839
|
+
});
|
|
565
840
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
566
841
|
}
|
|
567
842
|
}
|
|
@@ -575,9 +850,7 @@ export class SequencerPublisher {
|
|
|
575
850
|
const logData = { ...checkpoint, reason };
|
|
576
851
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
577
852
|
|
|
578
|
-
const attestationsAndSigners =
|
|
579
|
-
validationResult.attestations,
|
|
580
|
-
).getPackedAttestations();
|
|
853
|
+
const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
|
|
581
854
|
|
|
582
855
|
if (reason === 'invalid-attestation') {
|
|
583
856
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -604,27 +877,10 @@ export class SequencerPublisher {
|
|
|
604
877
|
checkpoint: Checkpoint,
|
|
605
878
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
606
879
|
attestationsAndSignersSignature: Signature,
|
|
607
|
-
|
|
608
|
-
): Promise<
|
|
609
|
-
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
610
|
-
|
|
611
|
-
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
612
|
-
// If we have no attestations, we still need to provide the empty attestations
|
|
613
|
-
// so that the committee is recalculated correctly
|
|
614
|
-
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
615
|
-
// if (ignoreSignatures) {
|
|
616
|
-
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
617
|
-
// if (!committee) {
|
|
618
|
-
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
619
|
-
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
620
|
-
// }
|
|
621
|
-
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
622
|
-
// CommitteeAttestation.fromAddress(committeeMember),
|
|
623
|
-
// );
|
|
624
|
-
// }
|
|
625
|
-
|
|
880
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
881
|
+
): Promise<void> {
|
|
626
882
|
const blobFields = checkpoint.toBlobFields();
|
|
627
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
883
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
628
884
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
629
885
|
|
|
630
886
|
const args = [
|
|
@@ -632,7 +888,7 @@ export class SequencerPublisher {
|
|
|
632
888
|
header: checkpoint.header.toViem(),
|
|
633
889
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
634
890
|
oracleInput: {
|
|
635
|
-
feeAssetPriceModifier:
|
|
891
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
636
892
|
},
|
|
637
893
|
},
|
|
638
894
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -641,13 +897,11 @@ export class SequencerPublisher {
|
|
|
641
897
|
blobInput,
|
|
642
898
|
] as const;
|
|
643
899
|
|
|
644
|
-
await this.simulateProposeTx(args,
|
|
645
|
-
return ts;
|
|
900
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
646
901
|
}
|
|
647
902
|
|
|
648
903
|
private async enqueueCastSignalHelper(
|
|
649
904
|
slotNumber: SlotNumber,
|
|
650
|
-
timestamp: bigint,
|
|
651
905
|
signalType: GovernanceSignalAction,
|
|
652
906
|
payload: EthAddress,
|
|
653
907
|
base: IEmpireBase,
|
|
@@ -681,6 +935,32 @@ export class SequencerPublisher {
|
|
|
681
935
|
return false;
|
|
682
936
|
}
|
|
683
937
|
|
|
938
|
+
// Check if payload was already submitted to governance
|
|
939
|
+
const cacheKey = payload.toString();
|
|
940
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
941
|
+
try {
|
|
942
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
943
|
+
const proposed = await retry(
|
|
944
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
945
|
+
'Check if payload was proposed',
|
|
946
|
+
makeBackoff([0, 1, 2]),
|
|
947
|
+
this.log,
|
|
948
|
+
true,
|
|
949
|
+
);
|
|
950
|
+
if (proposed) {
|
|
951
|
+
this.payloadProposedCache.add(cacheKey);
|
|
952
|
+
}
|
|
953
|
+
} catch (err) {
|
|
954
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
960
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
|
|
684
964
|
const cachedLastVote = this.lastActions[signalType];
|
|
685
965
|
this.lastActions[signalType] = slotNumber;
|
|
686
966
|
const action = signalType;
|
|
@@ -699,11 +979,30 @@ export class SequencerPublisher {
|
|
|
699
979
|
lastValidL2Slot: slotNumber,
|
|
700
980
|
});
|
|
701
981
|
|
|
982
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
983
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
984
|
+
|
|
702
985
|
try {
|
|
703
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
986
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
704
987
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
705
988
|
} catch (err) {
|
|
706
|
-
|
|
989
|
+
const viemError = formatViemError(err);
|
|
990
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
|
|
991
|
+
simulationTimestamp: timestamp,
|
|
992
|
+
l1BlockNumber,
|
|
993
|
+
});
|
|
994
|
+
this.backupFailedTx({
|
|
995
|
+
id: keccak256(request.data!),
|
|
996
|
+
failureType: 'simulation',
|
|
997
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
998
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
999
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1000
|
+
context: {
|
|
1001
|
+
actions: [action],
|
|
1002
|
+
slot: slotNumber,
|
|
1003
|
+
sender: this.getSenderAddress().toString(),
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
707
1006
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
708
1007
|
}
|
|
709
1008
|
|
|
@@ -754,19 +1053,16 @@ export class SequencerPublisher {
|
|
|
754
1053
|
/**
|
|
755
1054
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
756
1055
|
* @param slotNumber - The slot number to cast a signal for.
|
|
757
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
758
1056
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
759
1057
|
*/
|
|
760
1058
|
public enqueueGovernanceCastSignal(
|
|
761
1059
|
governancePayload: EthAddress,
|
|
762
1060
|
slotNumber: SlotNumber,
|
|
763
|
-
timestamp: bigint,
|
|
764
1061
|
signerAddress: EthAddress,
|
|
765
1062
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
766
1063
|
): Promise<boolean> {
|
|
767
1064
|
return this.enqueueCastSignalHelper(
|
|
768
1065
|
slotNumber,
|
|
769
|
-
timestamp,
|
|
770
1066
|
'governance-signal',
|
|
771
1067
|
governancePayload,
|
|
772
1068
|
this.govProposerContract,
|
|
@@ -779,7 +1075,6 @@ export class SequencerPublisher {
|
|
|
779
1075
|
public async enqueueSlashingActions(
|
|
780
1076
|
actions: ProposerSlashAction[],
|
|
781
1077
|
slotNumber: SlotNumber,
|
|
782
|
-
timestamp: bigint,
|
|
783
1078
|
signerAddress: EthAddress,
|
|
784
1079
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
785
1080
|
): Promise<boolean> {
|
|
@@ -790,58 +1085,6 @@ export class SequencerPublisher {
|
|
|
790
1085
|
|
|
791
1086
|
for (const action of actions) {
|
|
792
1087
|
switch (action.type) {
|
|
793
|
-
case 'vote-empire-payload': {
|
|
794
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
795
|
-
this.log.error('Cannot vote for empire payload on non-empire slashing contract');
|
|
796
|
-
break;
|
|
797
|
-
}
|
|
798
|
-
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
799
|
-
signerAddress,
|
|
800
|
-
});
|
|
801
|
-
await this.enqueueCastSignalHelper(
|
|
802
|
-
slotNumber,
|
|
803
|
-
timestamp,
|
|
804
|
-
'empire-slashing-signal',
|
|
805
|
-
action.payload,
|
|
806
|
-
this.slashingProposerContract,
|
|
807
|
-
signerAddress,
|
|
808
|
-
signer,
|
|
809
|
-
);
|
|
810
|
-
break;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
case 'create-empire-payload': {
|
|
814
|
-
this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
815
|
-
const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
|
|
816
|
-
await this.simulateAndEnqueueRequest(
|
|
817
|
-
'create-empire-payload',
|
|
818
|
-
request,
|
|
819
|
-
(receipt: TransactionReceipt) =>
|
|
820
|
-
!!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
|
|
821
|
-
slotNumber,
|
|
822
|
-
timestamp,
|
|
823
|
-
);
|
|
824
|
-
break;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
case 'execute-empire-payload': {
|
|
828
|
-
this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
|
|
829
|
-
if (this.slashingProposerContract?.type !== 'empire') {
|
|
830
|
-
this.log.error('Cannot execute slashing payload on non-empire slashing contract');
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
|
-
const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
|
|
834
|
-
const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
|
|
835
|
-
await this.simulateAndEnqueueRequest(
|
|
836
|
-
'execute-empire-payload',
|
|
837
|
-
request,
|
|
838
|
-
(receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
|
|
839
|
-
slotNumber,
|
|
840
|
-
timestamp,
|
|
841
|
-
);
|
|
842
|
-
break;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
1088
|
case 'vote-offenses': {
|
|
846
1089
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
847
1090
|
slotNumber,
|
|
@@ -849,19 +1092,17 @@ export class SequencerPublisher {
|
|
|
849
1092
|
votesCount: action.votes.length,
|
|
850
1093
|
signerAddress,
|
|
851
1094
|
});
|
|
852
|
-
if (this.slashingProposerContract
|
|
853
|
-
this.log.error('
|
|
1095
|
+
if (!this.slashingProposerContract) {
|
|
1096
|
+
this.log.error('No slashing proposer contract available');
|
|
854
1097
|
return false;
|
|
855
1098
|
}
|
|
856
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
857
1099
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
858
|
-
const request = await
|
|
1100
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
859
1101
|
await this.simulateAndEnqueueRequest(
|
|
860
1102
|
'vote-offenses',
|
|
861
1103
|
request,
|
|
862
|
-
(receipt: TransactionReceipt) => !!
|
|
1104
|
+
(receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
|
|
863
1105
|
slotNumber,
|
|
864
|
-
timestamp,
|
|
865
1106
|
);
|
|
866
1107
|
break;
|
|
867
1108
|
}
|
|
@@ -872,18 +1113,20 @@ export class SequencerPublisher {
|
|
|
872
1113
|
round: action.round,
|
|
873
1114
|
signerAddress,
|
|
874
1115
|
});
|
|
875
|
-
if (this.slashingProposerContract
|
|
876
|
-
this.log.error('
|
|
1116
|
+
if (!this.slashingProposerContract) {
|
|
1117
|
+
this.log.error('No slashing proposer contract available');
|
|
877
1118
|
return false;
|
|
878
1119
|
}
|
|
879
|
-
const
|
|
880
|
-
|
|
1120
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1121
|
+
action.round,
|
|
1122
|
+
action.committees,
|
|
1123
|
+
);
|
|
881
1124
|
await this.simulateAndEnqueueRequest(
|
|
882
1125
|
'execute-slash',
|
|
883
|
-
|
|
884
|
-
(receipt: TransactionReceipt) =>
|
|
1126
|
+
executeRequest,
|
|
1127
|
+
(receipt: TransactionReceipt) =>
|
|
1128
|
+
!!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
|
|
885
1129
|
slotNumber,
|
|
886
|
-
timestamp,
|
|
887
1130
|
);
|
|
888
1131
|
break;
|
|
889
1132
|
}
|
|
@@ -903,22 +1146,25 @@ export class SequencerPublisher {
|
|
|
903
1146
|
checkpoint: Checkpoint,
|
|
904
1147
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
905
1148
|
attestationsAndSignersSignature: Signature,
|
|
906
|
-
opts:
|
|
1149
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
907
1150
|
): Promise<void> {
|
|
908
1151
|
const checkpointHeader = checkpoint.header;
|
|
909
1152
|
|
|
910
1153
|
const blobFields = checkpoint.toBlobFields();
|
|
911
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1154
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
912
1155
|
|
|
913
|
-
const proposeTxArgs = {
|
|
1156
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
914
1157
|
header: checkpointHeader,
|
|
915
1158
|
archive: checkpoint.archive.root.toBuffer(),
|
|
916
1159
|
blobs,
|
|
917
1160
|
attestationsAndSigners,
|
|
918
1161
|
attestationsAndSignersSignature,
|
|
1162
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
919
1163
|
};
|
|
920
1164
|
|
|
921
|
-
|
|
1165
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
|
|
1166
|
+
.withoutBlobCheck()
|
|
1167
|
+
.build();
|
|
922
1168
|
|
|
923
1169
|
try {
|
|
924
1170
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
@@ -926,23 +1172,47 @@ export class SequencerPublisher {
|
|
|
926
1172
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
927
1173
|
// make time consistency checks break.
|
|
928
1174
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
929
|
-
|
|
1175
|
+
await this.validateCheckpointForSubmission(
|
|
930
1176
|
checkpoint,
|
|
931
1177
|
attestationsAndSigners,
|
|
932
1178
|
attestationsAndSignersSignature,
|
|
933
|
-
|
|
1179
|
+
simulationOverridesPlan,
|
|
934
1180
|
);
|
|
935
1181
|
} catch (err: any) {
|
|
936
1182
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
937
1183
|
...checkpoint.getStats(),
|
|
938
1184
|
slotNumber: checkpoint.header.slotNumber,
|
|
939
|
-
|
|
1185
|
+
simulationOverridesPlan,
|
|
940
1186
|
});
|
|
941
1187
|
throw err;
|
|
942
1188
|
}
|
|
943
1189
|
|
|
944
|
-
|
|
945
|
-
|
|
1190
|
+
// Build a pre-check callback that re-validates the checkpoint before L1 submission.
|
|
1191
|
+
// During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
|
|
1192
|
+
let preCheck = undefined;
|
|
1193
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
1194
|
+
preCheck = async () => {
|
|
1195
|
+
this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
|
|
1196
|
+
await this.validateCheckpointForSubmission(
|
|
1197
|
+
checkpoint,
|
|
1198
|
+
attestationsAndSigners,
|
|
1199
|
+
attestationsAndSignersSignature,
|
|
1200
|
+
simulationOverridesPlan,
|
|
1201
|
+
);
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
1206
|
+
...checkpoint.toCheckpointInfo(),
|
|
1207
|
+
txTimeoutAt: opts.txTimeoutAt,
|
|
1208
|
+
simulationOverridesPlan,
|
|
1209
|
+
});
|
|
1210
|
+
await this.addProposeTx(
|
|
1211
|
+
checkpoint,
|
|
1212
|
+
proposeTxArgs,
|
|
1213
|
+
{ txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
|
|
1214
|
+
preCheck,
|
|
1215
|
+
);
|
|
946
1216
|
}
|
|
947
1217
|
|
|
948
1218
|
public enqueueInvalidateCheckpoint(
|
|
@@ -985,8 +1255,8 @@ export class SequencerPublisher {
|
|
|
985
1255
|
request: L1TxRequest,
|
|
986
1256
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
987
1257
|
slotNumber: SlotNumber,
|
|
988
|
-
timestamp: bigint,
|
|
989
1258
|
) {
|
|
1259
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
990
1260
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
991
1261
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
992
1262
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
@@ -998,13 +1268,31 @@ export class SequencerPublisher {
|
|
|
998
1268
|
|
|
999
1269
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1000
1270
|
|
|
1271
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1272
|
+
|
|
1001
1273
|
let gasUsed: bigint;
|
|
1274
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1275
|
+
|
|
1002
1276
|
try {
|
|
1003
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1277
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1004
1278
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1005
1279
|
} catch (err) {
|
|
1006
|
-
const viemError = formatViemError(err);
|
|
1280
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1007
1281
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1282
|
+
|
|
1283
|
+
this.backupFailedTx({
|
|
1284
|
+
id: keccak256(request.data!),
|
|
1285
|
+
failureType: 'simulation',
|
|
1286
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1287
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1288
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1289
|
+
context: {
|
|
1290
|
+
actions: [action],
|
|
1291
|
+
slot: slotNumber,
|
|
1292
|
+
sender: this.getSenderAddress().toString(),
|
|
1293
|
+
},
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1008
1296
|
return false;
|
|
1009
1297
|
}
|
|
1010
1298
|
|
|
@@ -1012,10 +1300,14 @@ export class SequencerPublisher {
|
|
|
1012
1300
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1013
1301
|
logData.gasLimit = gasLimit;
|
|
1014
1302
|
|
|
1303
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1304
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1305
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1306
|
+
|
|
1015
1307
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1016
1308
|
this.addRequest({
|
|
1017
1309
|
action,
|
|
1018
|
-
request,
|
|
1310
|
+
request: requestWithAbi,
|
|
1019
1311
|
gasConfig: { gasLimit },
|
|
1020
1312
|
lastValidL2Slot: slotNumber,
|
|
1021
1313
|
checkSuccess: (_req, result) => {
|
|
@@ -1040,6 +1332,7 @@ export class SequencerPublisher {
|
|
|
1040
1332
|
*/
|
|
1041
1333
|
public interrupt() {
|
|
1042
1334
|
this.interrupted = true;
|
|
1335
|
+
this.interruptibleSleep.interrupt();
|
|
1043
1336
|
this.l1TxUtils.interrupt();
|
|
1044
1337
|
}
|
|
1045
1338
|
|
|
@@ -1049,11 +1342,7 @@ export class SequencerPublisher {
|
|
|
1049
1342
|
this.l1TxUtils.restart();
|
|
1050
1343
|
}
|
|
1051
1344
|
|
|
1052
|
-
private async prepareProposeTx(
|
|
1053
|
-
encodedData: L1ProcessArgs,
|
|
1054
|
-
timestamp: bigint,
|
|
1055
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1056
|
-
) {
|
|
1345
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
1057
1346
|
const kzg = Blob.getViemKzgInstance();
|
|
1058
1347
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1059
1348
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1084,9 +1373,27 @@ export class SequencerPublisher {
|
|
|
1084
1373
|
kzg,
|
|
1085
1374
|
},
|
|
1086
1375
|
)
|
|
1087
|
-
.catch(err => {
|
|
1088
|
-
const
|
|
1089
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1376
|
+
.catch(async err => {
|
|
1377
|
+
const viemError = formatViemError(err);
|
|
1378
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1379
|
+
const validateBlobsData = encodeFunctionData({
|
|
1380
|
+
abi: RollupAbi,
|
|
1381
|
+
functionName: 'validateBlobs',
|
|
1382
|
+
args: [blobInput],
|
|
1383
|
+
});
|
|
1384
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1385
|
+
this.backupFailedTx({
|
|
1386
|
+
id: keccak256(validateBlobsData),
|
|
1387
|
+
failureType: 'simulation',
|
|
1388
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1389
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1390
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1391
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1392
|
+
context: {
|
|
1393
|
+
actions: ['validate-blobs'],
|
|
1394
|
+
sender: this.getSenderAddress().toString(),
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1090
1397
|
throw new Error('Failed to validate blobs');
|
|
1091
1398
|
});
|
|
1092
1399
|
}
|
|
@@ -1097,8 +1404,7 @@ export class SequencerPublisher {
|
|
|
1097
1404
|
header: encodedData.header.toViem(),
|
|
1098
1405
|
archive: toHex(encodedData.archive),
|
|
1099
1406
|
oracleInput: {
|
|
1100
|
-
|
|
1101
|
-
feeAssetPriceModifier: 0n,
|
|
1407
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1102
1408
|
},
|
|
1103
1409
|
},
|
|
1104
1410
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1107,7 +1413,7 @@ export class SequencerPublisher {
|
|
|
1107
1413
|
blobInput,
|
|
1108
1414
|
] as const;
|
|
1109
1415
|
|
|
1110
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1416
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1111
1417
|
|
|
1112
1418
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1113
1419
|
}
|
|
@@ -1115,7 +1421,6 @@ export class SequencerPublisher {
|
|
|
1115
1421
|
/**
|
|
1116
1422
|
* Simulates the propose tx with eth_simulateV1
|
|
1117
1423
|
* @param args - The propose tx args
|
|
1118
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1119
1424
|
* @returns The simulation result
|
|
1120
1425
|
*/
|
|
1121
1426
|
private async simulateProposeTx(
|
|
@@ -1124,7 +1429,7 @@ export class SequencerPublisher {
|
|
|
1124
1429
|
readonly header: ViemHeader;
|
|
1125
1430
|
readonly archive: `0x${string}`;
|
|
1126
1431
|
readonly oracleInput: {
|
|
1127
|
-
readonly feeAssetPriceModifier:
|
|
1432
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1128
1433
|
};
|
|
1129
1434
|
},
|
|
1130
1435
|
ViemCommitteeAttestations,
|
|
@@ -1132,8 +1437,7 @@ export class SequencerPublisher {
|
|
|
1132
1437
|
ViemSignature,
|
|
1133
1438
|
`0x${string}`,
|
|
1134
1439
|
],
|
|
1135
|
-
|
|
1136
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1440
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
1137
1441
|
) {
|
|
1138
1442
|
const rollupData = encodeFunctionData({
|
|
1139
1443
|
abi: RollupAbi,
|
|
@@ -1141,23 +1445,7 @@ export class SequencerPublisher {
|
|
|
1141
1445
|
args,
|
|
1142
1446
|
});
|
|
1143
1447
|
|
|
1144
|
-
|
|
1145
|
-
const forcePendingCheckpointNumberStateDiff = (
|
|
1146
|
-
options.forcePendingCheckpointNumber !== undefined
|
|
1147
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1148
|
-
: []
|
|
1149
|
-
).flatMap(override => override.stateDiff ?? []);
|
|
1150
|
-
|
|
1151
|
-
const stateOverrides: StateOverride = [
|
|
1152
|
-
{
|
|
1153
|
-
address: this.rollupContract.address,
|
|
1154
|
-
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1155
|
-
stateDiff: [
|
|
1156
|
-
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1157
|
-
...forcePendingCheckpointNumberStateDiff,
|
|
1158
|
-
],
|
|
1159
|
-
},
|
|
1160
|
-
];
|
|
1448
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1161
1449
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1162
1450
|
if (this.proposerAddressForSimulation) {
|
|
1163
1451
|
stateOverrides.push({
|
|
@@ -1166,25 +1454,27 @@ export class SequencerPublisher {
|
|
|
1166
1454
|
});
|
|
1167
1455
|
}
|
|
1168
1456
|
|
|
1457
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1458
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1459
|
+
|
|
1169
1460
|
const simulationResult = await this.l1TxUtils
|
|
1170
1461
|
.simulate(
|
|
1171
1462
|
{
|
|
1172
1463
|
to: this.rollupContract.address,
|
|
1173
1464
|
data: rollupData,
|
|
1174
|
-
gas:
|
|
1465
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1175
1466
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1176
1467
|
},
|
|
1177
1468
|
{
|
|
1178
|
-
|
|
1179
|
-
time: timestamp + 1n,
|
|
1469
|
+
time: simTs,
|
|
1180
1470
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1181
|
-
gasLimit:
|
|
1471
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1182
1472
|
},
|
|
1183
1473
|
stateOverrides,
|
|
1184
1474
|
RollupAbi,
|
|
1185
1475
|
{
|
|
1186
1476
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1187
|
-
fallbackGasEstimate:
|
|
1477
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1188
1478
|
},
|
|
1189
1479
|
)
|
|
1190
1480
|
.catch(err => {
|
|
@@ -1194,11 +1484,23 @@ export class SequencerPublisher {
|
|
|
1194
1484
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1195
1485
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1196
1486
|
return {
|
|
1197
|
-
gasUsed:
|
|
1487
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1198
1488
|
logs: [],
|
|
1199
1489
|
};
|
|
1200
1490
|
}
|
|
1201
|
-
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1491
|
+
this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
|
|
1492
|
+
this.backupFailedTx({
|
|
1493
|
+
id: keccak256(rollupData),
|
|
1494
|
+
failureType: 'simulation',
|
|
1495
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1496
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1497
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1498
|
+
context: {
|
|
1499
|
+
actions: ['propose'],
|
|
1500
|
+
slot: Number(args[0].header.slotNumber),
|
|
1501
|
+
sender: this.getSenderAddress().toString(),
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1202
1504
|
throw err;
|
|
1203
1505
|
});
|
|
1204
1506
|
|
|
@@ -1208,16 +1510,15 @@ export class SequencerPublisher {
|
|
|
1208
1510
|
private async addProposeTx(
|
|
1209
1511
|
checkpoint: Checkpoint,
|
|
1210
1512
|
encodedData: L1ProcessArgs,
|
|
1211
|
-
opts:
|
|
1212
|
-
|
|
1513
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1514
|
+
preCheck?: () => Promise<void>,
|
|
1213
1515
|
): Promise<void> {
|
|
1214
1516
|
const slot = checkpoint.header.slotNumber;
|
|
1215
1517
|
const timer = new Timer();
|
|
1216
1518
|
const kzg = Blob.getViemKzgInstance();
|
|
1217
1519
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1218
1520
|
encodedData,
|
|
1219
|
-
|
|
1220
|
-
opts,
|
|
1521
|
+
opts.simulationOverridesPlan,
|
|
1221
1522
|
);
|
|
1222
1523
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1223
1524
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
@@ -1241,7 +1542,8 @@ export class SequencerPublisher {
|
|
|
1241
1542
|
data: rollupData,
|
|
1242
1543
|
},
|
|
1243
1544
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1244
|
-
gasConfig: {
|
|
1545
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
|
|
1546
|
+
preCheck,
|
|
1245
1547
|
blobConfig: {
|
|
1246
1548
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1247
1549
|
kzg,
|
|
@@ -1294,4 +1596,17 @@ export class SequencerPublisher {
|
|
|
1294
1596
|
},
|
|
1295
1597
|
});
|
|
1296
1598
|
}
|
|
1599
|
+
|
|
1600
|
+
/** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
|
|
1601
|
+
* for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
|
|
1602
|
+
private getSimulationTimestamp(slot: SlotNumber): bigint {
|
|
1603
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1604
|
+
return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** Returns the timestamp of the next L1 slot boundary after now. */
|
|
1608
|
+
private getNextL1SlotTimestamp(): bigint {
|
|
1609
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1610
|
+
return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1611
|
+
}
|
|
1297
1612
|
}
|