@aztec/sequencer-client 0.0.1-commit.e558bd1c → 0.0.1-commit.e5a3663dd
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.d.ts +75 -48
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +434 -141
- 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 +46 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +705 -223
- 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 +117 -21
- package/dest/sequencer/sequencer.d.ts +53 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +188 -101
- 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 +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +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.ts +501 -202
- package/src/sequencer/README.md +82 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +873 -243
- 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 +132 -25
- package/src/sequencer/sequencer.ts +262 -115
- 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 +63 -49
- package/src/test/utils.ts +31 -10
|
@@ -3,51 +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,
|
|
21
25
|
MAX_L1_TX_LIMIT,
|
|
22
26
|
type TransactionStats,
|
|
23
27
|
WEI_CONST,
|
|
24
28
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
29
|
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
27
30
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
28
|
-
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
29
31
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
32
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
30
33
|
import { pick } from '@aztec/foundation/collection';
|
|
31
34
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
35
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
32
36
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
33
37
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
34
38
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
39
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
40
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
35
41
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
36
|
-
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
42
|
+
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
37
43
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
38
44
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
39
45
|
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
40
46
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
41
|
-
import {
|
|
47
|
+
import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
42
48
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
49
|
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
44
50
|
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
45
51
|
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
|
|
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';
|
|
49
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
50
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
|
+
|
|
51
80
|
/** Arguments to the process method of the rollup contract */
|
|
52
81
|
type L1ProcessArgs = {
|
|
53
82
|
/** The L2 block header. */
|
|
@@ -60,6 +89,8 @@ type L1ProcessArgs = {
|
|
|
60
89
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
61
90
|
/** Attestations and signers signature */
|
|
62
91
|
attestationsAndSignersSignature: Signature;
|
|
92
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
93
|
+
feeAssetPriceModifier: bigint;
|
|
63
94
|
};
|
|
64
95
|
|
|
65
96
|
export const Actions = [
|
|
@@ -67,16 +98,13 @@ export const Actions = [
|
|
|
67
98
|
'invalidate-by-insufficient-attestations',
|
|
68
99
|
'propose',
|
|
69
100
|
'governance-signal',
|
|
70
|
-
'empire-slashing-signal',
|
|
71
|
-
'create-empire-payload',
|
|
72
|
-
'execute-empire-payload',
|
|
73
101
|
'vote-offenses',
|
|
74
102
|
'execute-slash',
|
|
75
103
|
] as const;
|
|
76
104
|
|
|
77
105
|
export type Action = (typeof Actions)[number];
|
|
78
106
|
|
|
79
|
-
type GovernanceSignalAction = Extract<Action, 'governance-signal'
|
|
107
|
+
type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
|
|
80
108
|
|
|
81
109
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
82
110
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
@@ -87,6 +115,13 @@ export type InvalidateCheckpointRequest = {
|
|
|
87
115
|
gasUsed: bigint;
|
|
88
116
|
checkpointNumber: CheckpointNumber;
|
|
89
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;
|
|
90
125
|
};
|
|
91
126
|
|
|
92
127
|
interface RequestWithExpiry {
|
|
@@ -95,6 +130,8 @@ interface RequestWithExpiry {
|
|
|
95
130
|
lastValidL2Slot: SlotNumber;
|
|
96
131
|
gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
|
|
97
132
|
blobConfig?: L1BlobInputs;
|
|
133
|
+
/** Optional pre-send validation. If it rejects, the request is discarded. */
|
|
134
|
+
preCheck?: () => Promise<void>;
|
|
98
135
|
checkSuccess: (
|
|
99
136
|
request: L1TxRequest,
|
|
100
137
|
result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
|
|
@@ -105,6 +142,7 @@ export class SequencerPublisher {
|
|
|
105
142
|
private interrupted = false;
|
|
106
143
|
private metrics: SequencerPublisherMetrics;
|
|
107
144
|
public epochCache: EpochCache;
|
|
145
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
108
146
|
|
|
109
147
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
110
148
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
@@ -112,61 +150,80 @@ export class SequencerPublisher {
|
|
|
112
150
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
113
151
|
|
|
114
152
|
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
153
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
115
154
|
|
|
116
155
|
protected log: Logger;
|
|
117
156
|
protected ethereumSlotDuration: bigint;
|
|
157
|
+
protected aztecSlotDuration: bigint;
|
|
158
|
+
|
|
159
|
+
/** Date provider for wall-clock time. */
|
|
160
|
+
private readonly dateProvider: DateProvider;
|
|
118
161
|
|
|
119
162
|
private blobClient: BlobClientInterface;
|
|
120
163
|
|
|
121
164
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
122
165
|
private proposerAddressForSimulation?: EthAddress;
|
|
123
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
|
+
|
|
124
170
|
/** L1 fee analyzer for fisherman mode */
|
|
125
171
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
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();
|
|
178
|
+
|
|
126
179
|
// A CALL to a cold address is 2700 gas
|
|
127
180
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
128
181
|
|
|
129
182
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
130
183
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
131
184
|
|
|
132
|
-
public l1TxUtils:
|
|
185
|
+
public l1TxUtils: L1TxUtils;
|
|
133
186
|
public rollupContract: RollupContract;
|
|
134
187
|
public govProposerContract: GovernanceProposerContract;
|
|
135
|
-
public slashingProposerContract:
|
|
136
|
-
public slashFactoryContract: SlashFactoryContract;
|
|
188
|
+
public slashingProposerContract: SlashingProposerContract | undefined;
|
|
137
189
|
|
|
138
190
|
public readonly tracer: Tracer;
|
|
139
191
|
|
|
140
192
|
protected requests: RequestWithExpiry[] = [];
|
|
141
193
|
|
|
142
194
|
constructor(
|
|
143
|
-
private config:
|
|
195
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
196
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
144
197
|
deps: {
|
|
145
198
|
telemetry?: TelemetryClient;
|
|
146
199
|
blobClient: BlobClientInterface;
|
|
147
|
-
l1TxUtils:
|
|
200
|
+
l1TxUtils: L1TxUtils;
|
|
148
201
|
rollupContract: RollupContract;
|
|
149
|
-
slashingProposerContract:
|
|
202
|
+
slashingProposerContract: SlashingProposerContract | undefined;
|
|
150
203
|
governanceProposerContract: GovernanceProposerContract;
|
|
151
|
-
slashFactoryContract: SlashFactoryContract;
|
|
152
204
|
epochCache: EpochCache;
|
|
153
205
|
dateProvider: DateProvider;
|
|
154
206
|
metrics: SequencerPublisherMetrics;
|
|
155
207
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
156
208
|
log?: Logger;
|
|
209
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
157
210
|
},
|
|
158
211
|
) {
|
|
159
212
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
160
213
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
214
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
215
|
+
this.dateProvider = deps.dateProvider;
|
|
161
216
|
this.epochCache = deps.epochCache;
|
|
162
217
|
this.lastActions = deps.lastActions;
|
|
163
218
|
|
|
164
219
|
this.blobClient = deps.blobClient;
|
|
220
|
+
this.dateProvider = deps.dateProvider;
|
|
165
221
|
|
|
166
222
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
167
223
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
168
224
|
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
169
225
|
this.l1TxUtils = deps.l1TxUtils;
|
|
226
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
170
227
|
|
|
171
228
|
this.rollupContract = deps.rollupContract;
|
|
172
229
|
|
|
@@ -178,8 +235,6 @@ export class SequencerPublisher {
|
|
|
178
235
|
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
179
236
|
this.slashingProposerContract = newSlashingProposer;
|
|
180
237
|
});
|
|
181
|
-
this.slashFactoryContract = deps.slashFactoryContract;
|
|
182
|
-
|
|
183
238
|
// Initialize L1 fee analyzer for fisherman mode
|
|
184
239
|
if (config.fishermanMode) {
|
|
185
240
|
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
@@ -188,12 +243,52 @@ export class SequencerPublisher {
|
|
|
188
243
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
189
244
|
);
|
|
190
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
|
+
});
|
|
191
278
|
}
|
|
192
279
|
|
|
193
280
|
public getRollupContract(): RollupContract {
|
|
194
281
|
return this.rollupContract;
|
|
195
282
|
}
|
|
196
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
|
+
|
|
197
292
|
public getSenderAddress() {
|
|
198
293
|
return this.l1TxUtils.getSenderAddress();
|
|
199
294
|
}
|
|
@@ -218,7 +313,7 @@ export class SequencerPublisher {
|
|
|
218
313
|
}
|
|
219
314
|
|
|
220
315
|
public getCurrentL2Slot(): SlotNumber {
|
|
221
|
-
return this.epochCache.
|
|
316
|
+
return this.epochCache.getSlotNow();
|
|
222
317
|
}
|
|
223
318
|
|
|
224
319
|
/**
|
|
@@ -296,9 +391,10 @@ export class SequencerPublisher {
|
|
|
296
391
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
297
392
|
*/
|
|
298
393
|
@trackSpan('SequencerPublisher.sendRequests')
|
|
299
|
-
public async sendRequests() {
|
|
394
|
+
public async sendRequests(): Promise<SendRequestsResult | undefined> {
|
|
300
395
|
const requestsToProcess = [...this.requests];
|
|
301
396
|
this.requests = [];
|
|
397
|
+
|
|
302
398
|
if (this.interrupted || requestsToProcess.length === 0) {
|
|
303
399
|
return undefined;
|
|
304
400
|
}
|
|
@@ -331,8 +427,8 @@ export class SequencerPublisher {
|
|
|
331
427
|
// @note - we can only have one blob config per bundle
|
|
332
428
|
// find requests with gas and blob configs
|
|
333
429
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
334
|
-
const gasConfigs =
|
|
335
|
-
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);
|
|
336
432
|
|
|
337
433
|
if (blobConfigs.length > 1) {
|
|
338
434
|
throw new Error('Multiple blob configs found');
|
|
@@ -361,19 +457,36 @@ export class SequencerPublisher {
|
|
|
361
457
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
362
458
|
|
|
363
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
|
+
|
|
364
477
|
this.log.debug('Forwarding transactions', {
|
|
365
478
|
validRequests: validRequests.map(request => request.action),
|
|
366
479
|
txConfig,
|
|
367
480
|
});
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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,
|
|
375
489
|
);
|
|
376
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
377
490
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
378
491
|
} catch (err) {
|
|
379
492
|
const viemError = formatViemError(err);
|
|
@@ -391,16 +504,127 @@ export class SequencerPublisher {
|
|
|
391
504
|
}
|
|
392
505
|
}
|
|
393
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
|
+
|
|
394
595
|
private callbackBundledTransactions(
|
|
395
596
|
requests: RequestWithExpiry[],
|
|
396
|
-
result
|
|
597
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
598
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
397
599
|
) {
|
|
398
600
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
399
601
|
if (result instanceof FormattedViemError) {
|
|
400
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
|
+
});
|
|
401
616
|
return { failedActions: requests.map(r => r.action) };
|
|
402
617
|
} else {
|
|
403
|
-
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
|
+
});
|
|
404
628
|
const successfulActions: Action[] = [];
|
|
405
629
|
const failedActions: Action[] = [];
|
|
406
630
|
for (const request of requests) {
|
|
@@ -410,27 +634,54 @@ export class SequencerPublisher {
|
|
|
410
634
|
failedActions.push(request.action);
|
|
411
635
|
}
|
|
412
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
|
+
}
|
|
413
661
|
return { successfulActions, failedActions };
|
|
414
662
|
}
|
|
415
663
|
}
|
|
416
664
|
|
|
417
665
|
/**
|
|
418
|
-
* @notice Will call `
|
|
666
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
419
667
|
* @param tipArchive - The archive to check
|
|
420
668
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
421
669
|
*/
|
|
422
|
-
public
|
|
423
|
-
tipArchive: Fr,
|
|
424
|
-
msgSender: EthAddress,
|
|
425
|
-
opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
426
|
-
) {
|
|
670
|
+
public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
427
671
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
428
672
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
429
673
|
|
|
674
|
+
const pipelined = this.epochCache.isProposerPipeliningEnabled();
|
|
675
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
676
|
+
const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
|
|
677
|
+
|
|
430
678
|
return this.rollupContract
|
|
431
|
-
.
|
|
432
|
-
|
|
433
|
-
|
|
679
|
+
.canProposeAt(
|
|
680
|
+
tipArchive.toBuffer(),
|
|
681
|
+
msgSender.toString(),
|
|
682
|
+
nextL1SlotTs,
|
|
683
|
+
await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
|
|
684
|
+
)
|
|
434
685
|
.catch(err => {
|
|
435
686
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
436
687
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
|
|
@@ -442,6 +693,7 @@ export class SequencerPublisher {
|
|
|
442
693
|
return undefined;
|
|
443
694
|
});
|
|
444
695
|
}
|
|
696
|
+
|
|
445
697
|
/**
|
|
446
698
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
447
699
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
@@ -451,13 +703,13 @@ export class SequencerPublisher {
|
|
|
451
703
|
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
452
704
|
public async validateBlockHeader(
|
|
453
705
|
header: CheckpointHeader,
|
|
454
|
-
|
|
706
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
455
707
|
): Promise<void> {
|
|
456
708
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
457
709
|
|
|
458
710
|
const args = [
|
|
459
711
|
header.toViem(),
|
|
460
|
-
CommitteeAttestationsAndSigners.
|
|
712
|
+
CommitteeAttestationsAndSigners.packAttestations([]),
|
|
461
713
|
[], // no signers
|
|
462
714
|
Signature.empty().toViemSignature(),
|
|
463
715
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
@@ -465,10 +717,8 @@ export class SequencerPublisher {
|
|
|
465
717
|
flags,
|
|
466
718
|
] as const;
|
|
467
719
|
|
|
468
|
-
const ts =
|
|
469
|
-
const stateOverrides = await this.rollupContract
|
|
470
|
-
opts?.forcePendingCheckpointNumber,
|
|
471
|
-
);
|
|
720
|
+
const ts = this.getSimulationTimestamp(header.slotNumber);
|
|
721
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
472
722
|
let balance = 0n;
|
|
473
723
|
if (this.config.fishermanMode) {
|
|
474
724
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -488,7 +738,7 @@ export class SequencerPublisher {
|
|
|
488
738
|
data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
|
|
489
739
|
from: MULTI_CALL_3_ADDRESS,
|
|
490
740
|
},
|
|
491
|
-
{ time: ts
|
|
741
|
+
{ time: ts },
|
|
492
742
|
stateOverrides,
|
|
493
743
|
);
|
|
494
744
|
this.log.debug(`Simulated validateHeader`);
|
|
@@ -521,6 +771,8 @@ export class SequencerPublisher {
|
|
|
521
771
|
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
522
772
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
523
773
|
|
|
774
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
775
|
+
|
|
524
776
|
try {
|
|
525
777
|
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
526
778
|
request,
|
|
@@ -539,6 +791,7 @@ export class SequencerPublisher {
|
|
|
539
791
|
gasUsed,
|
|
540
792
|
checkpointNumber,
|
|
541
793
|
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
794
|
+
lastArchive: validationResult.checkpoint.lastArchive,
|
|
542
795
|
reason,
|
|
543
796
|
};
|
|
544
797
|
} catch (err) {
|
|
@@ -551,8 +804,8 @@ export class SequencerPublisher {
|
|
|
551
804
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
552
805
|
{ ...logData, request, error: viemError.message },
|
|
553
806
|
);
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
807
|
+
const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
808
|
+
if (latestProposedCheckpointNumber < checkpointNumber) {
|
|
556
809
|
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
557
810
|
return undefined;
|
|
558
811
|
} else {
|
|
@@ -572,6 +825,18 @@ export class SequencerPublisher {
|
|
|
572
825
|
|
|
573
826
|
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
574
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
|
+
});
|
|
575
840
|
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
576
841
|
}
|
|
577
842
|
}
|
|
@@ -585,9 +850,7 @@ export class SequencerPublisher {
|
|
|
585
850
|
const logData = { ...checkpoint, reason };
|
|
586
851
|
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
587
852
|
|
|
588
|
-
const attestationsAndSigners =
|
|
589
|
-
validationResult.attestations,
|
|
590
|
-
).getPackedAttestations();
|
|
853
|
+
const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
|
|
591
854
|
|
|
592
855
|
if (reason === 'invalid-attestation') {
|
|
593
856
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
@@ -614,27 +877,10 @@ export class SequencerPublisher {
|
|
|
614
877
|
checkpoint: Checkpoint,
|
|
615
878
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
616
879
|
attestationsAndSignersSignature: Signature,
|
|
617
|
-
|
|
618
|
-
): Promise<
|
|
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
|
-
|
|
880
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
881
|
+
): Promise<void> {
|
|
636
882
|
const blobFields = checkpoint.toBlobFields();
|
|
637
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
883
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
638
884
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
639
885
|
|
|
640
886
|
const args = [
|
|
@@ -642,7 +888,7 @@ export class SequencerPublisher {
|
|
|
642
888
|
header: checkpoint.header.toViem(),
|
|
643
889
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
644
890
|
oracleInput: {
|
|
645
|
-
feeAssetPriceModifier:
|
|
891
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
646
892
|
},
|
|
647
893
|
},
|
|
648
894
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -651,13 +897,11 @@ export class SequencerPublisher {
|
|
|
651
897
|
blobInput,
|
|
652
898
|
] as const;
|
|
653
899
|
|
|
654
|
-
await this.simulateProposeTx(args,
|
|
655
|
-
return ts;
|
|
900
|
+
await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
656
901
|
}
|
|
657
902
|
|
|
658
903
|
private async enqueueCastSignalHelper(
|
|
659
904
|
slotNumber: SlotNumber,
|
|
660
|
-
timestamp: bigint,
|
|
661
905
|
signalType: GovernanceSignalAction,
|
|
662
906
|
payload: EthAddress,
|
|
663
907
|
base: IEmpireBase,
|
|
@@ -691,6 +935,32 @@ export class SequencerPublisher {
|
|
|
691
935
|
return false;
|
|
692
936
|
}
|
|
693
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
|
+
|
|
694
964
|
const cachedLastVote = this.lastActions[signalType];
|
|
695
965
|
this.lastActions[signalType] = slotNumber;
|
|
696
966
|
const action = signalType;
|
|
@@ -709,11 +979,30 @@ export class SequencerPublisher {
|
|
|
709
979
|
lastValidL2Slot: slotNumber,
|
|
710
980
|
});
|
|
711
981
|
|
|
982
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
983
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
984
|
+
|
|
712
985
|
try {
|
|
713
986
|
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
714
987
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
715
988
|
} catch (err) {
|
|
716
|
-
|
|
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
|
+
});
|
|
717
1006
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
718
1007
|
}
|
|
719
1008
|
|
|
@@ -764,19 +1053,16 @@ export class SequencerPublisher {
|
|
|
764
1053
|
/**
|
|
765
1054
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
766
1055
|
* @param slotNumber - The slot number to cast a signal for.
|
|
767
|
-
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
768
1056
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
769
1057
|
*/
|
|
770
1058
|
public enqueueGovernanceCastSignal(
|
|
771
1059
|
governancePayload: EthAddress,
|
|
772
1060
|
slotNumber: SlotNumber,
|
|
773
|
-
timestamp: bigint,
|
|
774
1061
|
signerAddress: EthAddress,
|
|
775
1062
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
776
1063
|
): Promise<boolean> {
|
|
777
1064
|
return this.enqueueCastSignalHelper(
|
|
778
1065
|
slotNumber,
|
|
779
|
-
timestamp,
|
|
780
1066
|
'governance-signal',
|
|
781
1067
|
governancePayload,
|
|
782
1068
|
this.govProposerContract,
|
|
@@ -789,7 +1075,6 @@ export class SequencerPublisher {
|
|
|
789
1075
|
public async enqueueSlashingActions(
|
|
790
1076
|
actions: ProposerSlashAction[],
|
|
791
1077
|
slotNumber: SlotNumber,
|
|
792
|
-
timestamp: bigint,
|
|
793
1078
|
signerAddress: EthAddress,
|
|
794
1079
|
signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
|
|
795
1080
|
): Promise<boolean> {
|
|
@@ -800,58 +1085,6 @@ export class SequencerPublisher {
|
|
|
800
1085
|
|
|
801
1086
|
for (const action of actions) {
|
|
802
1087
|
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
1088
|
case 'vote-offenses': {
|
|
856
1089
|
this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
|
|
857
1090
|
slotNumber,
|
|
@@ -859,19 +1092,17 @@ export class SequencerPublisher {
|
|
|
859
1092
|
votesCount: action.votes.length,
|
|
860
1093
|
signerAddress,
|
|
861
1094
|
});
|
|
862
|
-
if (this.slashingProposerContract
|
|
863
|
-
this.log.error('
|
|
1095
|
+
if (!this.slashingProposerContract) {
|
|
1096
|
+
this.log.error('No slashing proposer contract available');
|
|
864
1097
|
return false;
|
|
865
1098
|
}
|
|
866
|
-
const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
|
|
867
1099
|
const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
|
|
868
|
-
const request = await
|
|
1100
|
+
const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
|
|
869
1101
|
await this.simulateAndEnqueueRequest(
|
|
870
1102
|
'vote-offenses',
|
|
871
1103
|
request,
|
|
872
|
-
(receipt: TransactionReceipt) => !!
|
|
1104
|
+
(receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
|
|
873
1105
|
slotNumber,
|
|
874
|
-
timestamp,
|
|
875
1106
|
);
|
|
876
1107
|
break;
|
|
877
1108
|
}
|
|
@@ -882,18 +1113,20 @@ export class SequencerPublisher {
|
|
|
882
1113
|
round: action.round,
|
|
883
1114
|
signerAddress,
|
|
884
1115
|
});
|
|
885
|
-
if (this.slashingProposerContract
|
|
886
|
-
this.log.error('
|
|
1116
|
+
if (!this.slashingProposerContract) {
|
|
1117
|
+
this.log.error('No slashing proposer contract available');
|
|
887
1118
|
return false;
|
|
888
1119
|
}
|
|
889
|
-
const
|
|
890
|
-
|
|
1120
|
+
const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
|
|
1121
|
+
action.round,
|
|
1122
|
+
action.committees,
|
|
1123
|
+
);
|
|
891
1124
|
await this.simulateAndEnqueueRequest(
|
|
892
1125
|
'execute-slash',
|
|
893
|
-
|
|
894
|
-
(receipt: TransactionReceipt) =>
|
|
1126
|
+
executeRequest,
|
|
1127
|
+
(receipt: TransactionReceipt) =>
|
|
1128
|
+
!!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
|
|
895
1129
|
slotNumber,
|
|
896
|
-
timestamp,
|
|
897
1130
|
);
|
|
898
1131
|
break;
|
|
899
1132
|
}
|
|
@@ -913,22 +1146,25 @@ export class SequencerPublisher {
|
|
|
913
1146
|
checkpoint: Checkpoint,
|
|
914
1147
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
915
1148
|
attestationsAndSignersSignature: Signature,
|
|
916
|
-
opts:
|
|
1149
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
917
1150
|
): Promise<void> {
|
|
918
1151
|
const checkpointHeader = checkpoint.header;
|
|
919
1152
|
|
|
920
1153
|
const blobFields = checkpoint.toBlobFields();
|
|
921
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1154
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
922
1155
|
|
|
923
|
-
const proposeTxArgs = {
|
|
1156
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
924
1157
|
header: checkpointHeader,
|
|
925
1158
|
archive: checkpoint.archive.root.toBuffer(),
|
|
926
1159
|
blobs,
|
|
927
1160
|
attestationsAndSigners,
|
|
928
1161
|
attestationsAndSignersSignature,
|
|
1162
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
929
1163
|
};
|
|
930
1164
|
|
|
931
|
-
|
|
1165
|
+
const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
|
|
1166
|
+
.withoutBlobCheck()
|
|
1167
|
+
.build();
|
|
932
1168
|
|
|
933
1169
|
try {
|
|
934
1170
|
// @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
|
|
@@ -936,23 +1172,47 @@ export class SequencerPublisher {
|
|
|
936
1172
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
937
1173
|
// make time consistency checks break.
|
|
938
1174
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
939
|
-
|
|
1175
|
+
await this.validateCheckpointForSubmission(
|
|
940
1176
|
checkpoint,
|
|
941
1177
|
attestationsAndSigners,
|
|
942
1178
|
attestationsAndSignersSignature,
|
|
943
|
-
|
|
1179
|
+
simulationOverridesPlan,
|
|
944
1180
|
);
|
|
945
1181
|
} catch (err: any) {
|
|
946
1182
|
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
947
1183
|
...checkpoint.getStats(),
|
|
948
1184
|
slotNumber: checkpoint.header.slotNumber,
|
|
949
|
-
|
|
1185
|
+
simulationOverridesPlan,
|
|
950
1186
|
});
|
|
951
1187
|
throw err;
|
|
952
1188
|
}
|
|
953
1189
|
|
|
954
|
-
|
|
955
|
-
|
|
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
|
+
);
|
|
956
1216
|
}
|
|
957
1217
|
|
|
958
1218
|
public enqueueInvalidateCheckpoint(
|
|
@@ -995,8 +1255,8 @@ export class SequencerPublisher {
|
|
|
995
1255
|
request: L1TxRequest,
|
|
996
1256
|
checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
|
|
997
1257
|
slotNumber: SlotNumber,
|
|
998
|
-
timestamp: bigint,
|
|
999
1258
|
) {
|
|
1259
|
+
const timestamp = this.getSimulationTimestamp(slotNumber);
|
|
1000
1260
|
const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
|
|
1001
1261
|
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
1002
1262
|
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
@@ -1008,15 +1268,31 @@ export class SequencerPublisher {
|
|
|
1008
1268
|
|
|
1009
1269
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1010
1270
|
|
|
1271
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1272
|
+
|
|
1011
1273
|
let gasUsed: bigint;
|
|
1012
1274
|
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1275
|
+
|
|
1013
1276
|
try {
|
|
1014
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1277
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
|
|
1015
1278
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1016
1279
|
} catch (err) {
|
|
1017
1280
|
const viemError = formatViemError(err, simulateAbi);
|
|
1018
1281
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1019
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
|
+
|
|
1020
1296
|
return false;
|
|
1021
1297
|
}
|
|
1022
1298
|
|
|
@@ -1056,6 +1332,7 @@ export class SequencerPublisher {
|
|
|
1056
1332
|
*/
|
|
1057
1333
|
public interrupt() {
|
|
1058
1334
|
this.interrupted = true;
|
|
1335
|
+
this.interruptibleSleep.interrupt();
|
|
1059
1336
|
this.l1TxUtils.interrupt();
|
|
1060
1337
|
}
|
|
1061
1338
|
|
|
@@ -1065,11 +1342,7 @@ export class SequencerPublisher {
|
|
|
1065
1342
|
this.l1TxUtils.restart();
|
|
1066
1343
|
}
|
|
1067
1344
|
|
|
1068
|
-
private async prepareProposeTx(
|
|
1069
|
-
encodedData: L1ProcessArgs,
|
|
1070
|
-
timestamp: bigint,
|
|
1071
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1072
|
-
) {
|
|
1345
|
+
private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
|
|
1073
1346
|
const kzg = Blob.getViemKzgInstance();
|
|
1074
1347
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
1075
1348
|
this.log.debug('Validating blob input', { blobInput });
|
|
@@ -1100,9 +1373,27 @@ export class SequencerPublisher {
|
|
|
1100
1373
|
kzg,
|
|
1101
1374
|
},
|
|
1102
1375
|
)
|
|
1103
|
-
.catch(err => {
|
|
1104
|
-
const
|
|
1105
|
-
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
|
+
});
|
|
1106
1397
|
throw new Error('Failed to validate blobs');
|
|
1107
1398
|
});
|
|
1108
1399
|
}
|
|
@@ -1113,8 +1404,7 @@ export class SequencerPublisher {
|
|
|
1113
1404
|
header: encodedData.header.toViem(),
|
|
1114
1405
|
archive: toHex(encodedData.archive),
|
|
1115
1406
|
oracleInput: {
|
|
1116
|
-
|
|
1117
|
-
feeAssetPriceModifier: 0n,
|
|
1407
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1118
1408
|
},
|
|
1119
1409
|
},
|
|
1120
1410
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1123,7 +1413,7 @@ export class SequencerPublisher {
|
|
|
1123
1413
|
blobInput,
|
|
1124
1414
|
] as const;
|
|
1125
1415
|
|
|
1126
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args,
|
|
1416
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
|
|
1127
1417
|
|
|
1128
1418
|
return { args, blobEvaluationGas, rollupData, simulationResult };
|
|
1129
1419
|
}
|
|
@@ -1131,7 +1421,6 @@ export class SequencerPublisher {
|
|
|
1131
1421
|
/**
|
|
1132
1422
|
* Simulates the propose tx with eth_simulateV1
|
|
1133
1423
|
* @param args - The propose tx args
|
|
1134
|
-
* @param timestamp - The timestamp to simulate proposal at
|
|
1135
1424
|
* @returns The simulation result
|
|
1136
1425
|
*/
|
|
1137
1426
|
private async simulateProposeTx(
|
|
@@ -1140,7 +1429,7 @@ export class SequencerPublisher {
|
|
|
1140
1429
|
readonly header: ViemHeader;
|
|
1141
1430
|
readonly archive: `0x${string}`;
|
|
1142
1431
|
readonly oracleInput: {
|
|
1143
|
-
readonly feeAssetPriceModifier:
|
|
1432
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1144
1433
|
};
|
|
1145
1434
|
},
|
|
1146
1435
|
ViemCommitteeAttestations,
|
|
@@ -1148,8 +1437,7 @@ export class SequencerPublisher {
|
|
|
1148
1437
|
ViemSignature,
|
|
1149
1438
|
`0x${string}`,
|
|
1150
1439
|
],
|
|
1151
|
-
|
|
1152
|
-
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1440
|
+
simulationOverridesPlan?: SimulationOverridesPlan,
|
|
1153
1441
|
) {
|
|
1154
1442
|
const rollupData = encodeFunctionData({
|
|
1155
1443
|
abi: RollupAbi,
|
|
@@ -1157,23 +1445,7 @@ export class SequencerPublisher {
|
|
|
1157
1445
|
args,
|
|
1158
1446
|
});
|
|
1159
1447
|
|
|
1160
|
-
|
|
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
|
-
];
|
|
1448
|
+
const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
|
|
1177
1449
|
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
1178
1450
|
if (this.proposerAddressForSimulation) {
|
|
1179
1451
|
stateOverrides.push({
|
|
@@ -1182,6 +1454,9 @@ export class SequencerPublisher {
|
|
|
1182
1454
|
});
|
|
1183
1455
|
}
|
|
1184
1456
|
|
|
1457
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1458
|
+
const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
|
|
1459
|
+
|
|
1185
1460
|
const simulationResult = await this.l1TxUtils
|
|
1186
1461
|
.simulate(
|
|
1187
1462
|
{
|
|
@@ -1191,8 +1466,7 @@ export class SequencerPublisher {
|
|
|
1191
1466
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1192
1467
|
},
|
|
1193
1468
|
{
|
|
1194
|
-
|
|
1195
|
-
time: timestamp + 1n,
|
|
1469
|
+
time: simTs,
|
|
1196
1470
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1197
1471
|
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1198
1472
|
},
|
|
@@ -1214,7 +1488,19 @@ export class SequencerPublisher {
|
|
|
1214
1488
|
logs: [],
|
|
1215
1489
|
};
|
|
1216
1490
|
}
|
|
1217
|
-
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
|
+
});
|
|
1218
1504
|
throw err;
|
|
1219
1505
|
});
|
|
1220
1506
|
|
|
@@ -1224,16 +1510,15 @@ export class SequencerPublisher {
|
|
|
1224
1510
|
private async addProposeTx(
|
|
1225
1511
|
checkpoint: Checkpoint,
|
|
1226
1512
|
encodedData: L1ProcessArgs,
|
|
1227
|
-
opts:
|
|
1228
|
-
|
|
1513
|
+
opts: EnqueueProposeCheckpointOpts = {},
|
|
1514
|
+
preCheck?: () => Promise<void>,
|
|
1229
1515
|
): Promise<void> {
|
|
1230
1516
|
const slot = checkpoint.header.slotNumber;
|
|
1231
1517
|
const timer = new Timer();
|
|
1232
1518
|
const kzg = Blob.getViemKzgInstance();
|
|
1233
1519
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
1234
1520
|
encodedData,
|
|
1235
|
-
|
|
1236
|
-
opts,
|
|
1521
|
+
opts.simulationOverridesPlan,
|
|
1237
1522
|
);
|
|
1238
1523
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
1239
1524
|
const gasLimit = this.l1TxUtils.bumpGasLimit(
|
|
@@ -1257,7 +1542,8 @@ export class SequencerPublisher {
|
|
|
1257
1542
|
data: rollupData,
|
|
1258
1543
|
},
|
|
1259
1544
|
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1260
|
-
gasConfig: {
|
|
1545
|
+
gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
|
|
1546
|
+
preCheck,
|
|
1261
1547
|
blobConfig: {
|
|
1262
1548
|
blobs: encodedData.blobs.map(b => b.data),
|
|
1263
1549
|
kzg,
|
|
@@ -1310,4 +1596,17 @@ export class SequencerPublisher {
|
|
|
1310
1596
|
},
|
|
1311
1597
|
});
|
|
1312
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
|
+
}
|
|
1313
1612
|
}
|