@aztec/sequencer-client 0.0.1-commit.9b94fc1 → 0.0.1-commit.9ee6fcc6
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 +24 -16
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +73 -32
- package/dest/config.d.ts +35 -9
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +106 -44
- package/dest/global_variable_builder/global_builder.d.ts +27 -14
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +65 -54
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/publisher/config.d.ts +53 -20
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +124 -39
- 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 +59 -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 +15 -6
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +28 -3
- package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +23 -86
- package/dest/publisher/sequencer-publisher.d.ts +79 -49
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +932 -155
- package/dest/sequencer/checkpoint_proposal_job.d.ts +108 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job.js +1289 -0
- package/dest/sequencer/checkpoint_voter.d.ts +35 -0
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_voter.js +109 -0
- package/dest/sequencer/config.d.ts +3 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/events.d.ts +47 -0
- package/dest/sequencer/events.d.ts.map +1 -0
- package/dest/sequencer/events.js +1 -0
- package/dest/sequencer/index.d.ts +4 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +3 -1
- package/dest/sequencer/metrics.d.ts +42 -6
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +227 -72
- package/dest/sequencer/sequencer.d.ts +125 -134
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +754 -652
- package/dest/sequencer/timetable.d.ts +54 -16
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +147 -62
- package/dest/sequencer/types.d.ts +3 -0
- package/dest/sequencer/types.d.ts.map +1 -0
- package/dest/sequencer/types.js +1 -0
- package/dest/sequencer/utils.d.ts +14 -8
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +7 -4
- package/dest/test/index.d.ts +6 -7
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +95 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +231 -0
- package/dest/test/utils.d.ts +53 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +104 -0
- package/package.json +31 -30
- package/src/client/sequencer-client.ts +97 -55
- package/src/config.ts +124 -53
- package/src/global_variable_builder/global_builder.ts +76 -73
- package/src/global_variable_builder/index.ts +1 -1
- package/src/index.ts +1 -7
- package/src/publisher/config.ts +163 -50
- 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 +55 -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 +43 -10
- package/src/publisher/sequencer-publisher-metrics.ts +19 -71
- package/src/publisher/sequencer-publisher.ts +635 -200
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/checkpoint_proposal_job.ts +1049 -0
- package/src/sequencer/checkpoint_voter.ts +130 -0
- package/src/sequencer/config.ts +2 -1
- package/src/sequencer/events.ts +27 -0
- package/src/sequencer/index.ts +3 -1
- package/src/sequencer/metrics.ts +282 -82
- package/src/sequencer/sequencer.ts +521 -859
- package/src/sequencer/timetable.ts +178 -83
- package/src/sequencer/types.ts +6 -0
- package/src/sequencer/utils.ts +18 -9
- package/src/test/index.ts +5 -6
- package/src/test/mock_checkpoint_builder.ts +323 -0
- package/src/test/utils.ts +167 -0
- package/dest/sequencer/block_builder.d.ts +0 -27
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -134
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -53
- package/src/sequencer/block_builder.ts +0 -222
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -132
|
@@ -1,48 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
3
|
-
import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
|
|
4
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
-
|
|
7
|
+
FeeAssetPriceOracle,
|
|
8
8
|
type GovernanceProposerContract,
|
|
9
9
|
type IEmpireBase,
|
|
10
|
-
type L1BlobInputs,
|
|
11
|
-
type L1ContractsConfig,
|
|
12
|
-
type L1TxConfig,
|
|
13
|
-
type L1TxRequest,
|
|
14
10
|
MULTI_CALL_3_ADDRESS,
|
|
15
11
|
Multicall3,
|
|
16
12
|
RollupContract,
|
|
17
13
|
type TallySlashingProposerContract,
|
|
18
|
-
type TransactionStats,
|
|
19
14
|
type ViemCommitteeAttestations,
|
|
20
15
|
type ViemHeader,
|
|
16
|
+
} from '@aztec/ethereum/contracts';
|
|
17
|
+
import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
18
|
+
import {
|
|
19
|
+
type L1BlobInputs,
|
|
20
|
+
type L1TxConfig,
|
|
21
|
+
type L1TxRequest,
|
|
22
|
+
type L1TxUtils,
|
|
23
|
+
MAX_L1_TX_LIMIT,
|
|
24
|
+
type TransactionStats,
|
|
21
25
|
WEI_CONST,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from '@aztec/ethereum';
|
|
25
|
-
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
26
|
+
} from '@aztec/ethereum/l1-tx-utils';
|
|
27
|
+
import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
26
28
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
27
29
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
28
|
-
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
30
|
+
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
31
|
+
import { trimmedBytesLength } from '@aztec/foundation/buffer';
|
|
32
|
+
import { pick } from '@aztec/foundation/collection';
|
|
33
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
34
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
29
35
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
30
36
|
import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
|
|
31
|
-
import type { Fr } from '@aztec/foundation/fields';
|
|
32
37
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
38
|
+
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
33
39
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
34
40
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
35
41
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
36
42
|
import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
37
|
-
import {
|
|
43
|
+
import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
44
|
+
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
45
|
+
import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
38
46
|
import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
|
|
39
47
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
40
|
-
import type {
|
|
41
|
-
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
48
|
+
import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
|
|
49
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
42
50
|
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
import {
|
|
52
|
+
type Hex,
|
|
53
|
+
type StateOverride,
|
|
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';
|
|
46
64
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
47
65
|
|
|
48
66
|
/** Arguments to the process method of the rollup contract */
|
|
@@ -57,6 +75,8 @@ type L1ProcessArgs = {
|
|
|
57
75
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
58
76
|
/** Attestations and signers signature */
|
|
59
77
|
attestationsAndSignersSignature: Signature;
|
|
78
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
79
|
+
feeAssetPriceModifier: bigint;
|
|
60
80
|
};
|
|
61
81
|
|
|
62
82
|
export const Actions = [
|
|
@@ -78,12 +98,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
|
|
|
78
98
|
// Sorting for actions such that invalidations go before proposals, and proposals go before votes
|
|
79
99
|
export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
|
|
80
100
|
|
|
81
|
-
export type
|
|
101
|
+
export type InvalidateCheckpointRequest = {
|
|
82
102
|
request: L1TxRequest;
|
|
83
103
|
reason: 'invalid-attestation' | 'insufficient-attestations';
|
|
84
104
|
gasUsed: bigint;
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
checkpointNumber: CheckpointNumber;
|
|
106
|
+
forcePendingCheckpointNumber: CheckpointNumber;
|
|
87
107
|
};
|
|
88
108
|
|
|
89
109
|
interface RequestWithExpiry {
|
|
@@ -102,23 +122,34 @@ export class SequencerPublisher {
|
|
|
102
122
|
private interrupted = false;
|
|
103
123
|
private metrics: SequencerPublisherMetrics;
|
|
104
124
|
public epochCache: EpochCache;
|
|
125
|
+
private failedTxStore?: Promise<L1TxFailedStore | undefined>;
|
|
105
126
|
|
|
106
127
|
protected governanceLog = createLogger('sequencer:publisher:governance');
|
|
107
128
|
protected slashingLog = createLogger('sequencer:publisher:slashing');
|
|
108
129
|
|
|
109
130
|
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
|
|
110
131
|
|
|
132
|
+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
|
|
133
|
+
private payloadProposedCache: Set<string> = new Set<string>();
|
|
134
|
+
|
|
111
135
|
protected log: Logger;
|
|
112
136
|
protected ethereumSlotDuration: bigint;
|
|
137
|
+
protected aztecSlotDuration: bigint;
|
|
138
|
+
private dateProvider: DateProvider;
|
|
113
139
|
|
|
114
|
-
private
|
|
140
|
+
private blobClient: BlobClientInterface;
|
|
115
141
|
|
|
116
142
|
/** Address to use for simulations in fisherman mode (actual proposer's address) */
|
|
117
143
|
private proposerAddressForSimulation?: EthAddress;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
|
|
145
|
+
/** Optional callback to obtain a replacement publisher when the current one fails to send. */
|
|
146
|
+
private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
147
|
+
|
|
148
|
+
/** L1 fee analyzer for fisherman mode */
|
|
149
|
+
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
150
|
+
|
|
151
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
152
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
122
153
|
|
|
123
154
|
// A CALL to a cold address is 2700 gas
|
|
124
155
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -126,20 +157,23 @@ export class SequencerPublisher {
|
|
|
126
157
|
// Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
|
|
127
158
|
public static VOTE_GAS_GUESS: bigint = 800_000n;
|
|
128
159
|
|
|
129
|
-
public l1TxUtils:
|
|
160
|
+
public l1TxUtils: L1TxUtils;
|
|
130
161
|
public rollupContract: RollupContract;
|
|
131
162
|
public govProposerContract: GovernanceProposerContract;
|
|
132
163
|
public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
133
164
|
public slashFactoryContract: SlashFactoryContract;
|
|
134
165
|
|
|
166
|
+
public readonly tracer: Tracer;
|
|
167
|
+
|
|
135
168
|
protected requests: RequestWithExpiry[] = [];
|
|
136
169
|
|
|
137
170
|
constructor(
|
|
138
|
-
private config:
|
|
171
|
+
private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
|
|
172
|
+
Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
|
|
139
173
|
deps: {
|
|
140
174
|
telemetry?: TelemetryClient;
|
|
141
|
-
|
|
142
|
-
l1TxUtils:
|
|
175
|
+
blobClient: BlobClientInterface;
|
|
176
|
+
l1TxUtils: L1TxUtils;
|
|
143
177
|
rollupContract: RollupContract;
|
|
144
178
|
slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
|
|
145
179
|
governanceProposerContract: GovernanceProposerContract;
|
|
@@ -149,19 +183,23 @@ export class SequencerPublisher {
|
|
|
149
183
|
metrics: SequencerPublisherMetrics;
|
|
150
184
|
lastActions: Partial<Record<Action, SlotNumber>>;
|
|
151
185
|
log?: Logger;
|
|
186
|
+
getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
|
|
152
187
|
},
|
|
153
188
|
) {
|
|
154
189
|
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
155
190
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
191
|
+
this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
|
|
192
|
+
this.dateProvider = deps.dateProvider;
|
|
156
193
|
this.epochCache = deps.epochCache;
|
|
157
194
|
this.lastActions = deps.lastActions;
|
|
158
195
|
|
|
159
|
-
this.
|
|
160
|
-
deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
|
|
196
|
+
this.blobClient = deps.blobClient;
|
|
161
197
|
|
|
162
198
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
163
199
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
200
|
+
this.tracer = telemetry.getTracer('SequencerPublisher');
|
|
164
201
|
this.l1TxUtils = deps.l1TxUtils;
|
|
202
|
+
this.getNextPublisher = deps.getNextPublisher;
|
|
165
203
|
|
|
166
204
|
this.rollupContract = deps.rollupContract;
|
|
167
205
|
|
|
@@ -174,16 +212,72 @@ export class SequencerPublisher {
|
|
|
174
212
|
this.slashingProposerContract = newSlashingProposer;
|
|
175
213
|
});
|
|
176
214
|
this.slashFactoryContract = deps.slashFactoryContract;
|
|
215
|
+
|
|
216
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
217
|
+
if (config.fishermanMode) {
|
|
218
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(
|
|
219
|
+
this.l1TxUtils.client,
|
|
220
|
+
deps.dateProvider,
|
|
221
|
+
createLogger('sequencer:publisher:fee-analyzer'),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Initialize fee asset price oracle
|
|
226
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
227
|
+
this.l1TxUtils.client,
|
|
228
|
+
this.rollupContract,
|
|
229
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Initialize failed L1 tx store (optional, for test networks)
|
|
233
|
+
this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Backs up a failed L1 transaction to the configured store for debugging.
|
|
238
|
+
* Does nothing if no store is configured.
|
|
239
|
+
*/
|
|
240
|
+
private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
|
|
241
|
+
if (!this.failedTxStore) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const tx: FailedL1Tx = {
|
|
246
|
+
...failedTx,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Fire and forget - don't block on backup
|
|
251
|
+
void this.failedTxStore
|
|
252
|
+
.then(store => store?.saveFailedTx(tx))
|
|
253
|
+
.catch(err => {
|
|
254
|
+
this.log.warn(`Failed to backup failed L1 tx to store`, err);
|
|
255
|
+
});
|
|
177
256
|
}
|
|
178
257
|
|
|
179
258
|
public getRollupContract(): RollupContract {
|
|
180
259
|
return this.rollupContract;
|
|
181
260
|
}
|
|
182
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Gets the fee asset price modifier from the oracle.
|
|
264
|
+
* Returns 0n if the oracle query fails.
|
|
265
|
+
*/
|
|
266
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
267
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
268
|
+
}
|
|
269
|
+
|
|
183
270
|
public getSenderAddress() {
|
|
184
271
|
return this.l1TxUtils.getSenderAddress();
|
|
185
272
|
}
|
|
186
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
276
|
+
*/
|
|
277
|
+
public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
|
|
278
|
+
return this.l1FeeAnalyzer;
|
|
279
|
+
}
|
|
280
|
+
|
|
187
281
|
/**
|
|
188
282
|
* Sets the proposer address to use for simulations in fisherman mode.
|
|
189
283
|
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
@@ -197,7 +291,7 @@ export class SequencerPublisher {
|
|
|
197
291
|
}
|
|
198
292
|
|
|
199
293
|
public getCurrentL2Slot(): SlotNumber {
|
|
200
|
-
return this.epochCache.
|
|
294
|
+
return this.epochCache.getSlotNow();
|
|
201
295
|
}
|
|
202
296
|
|
|
203
297
|
/**
|
|
@@ -211,6 +305,62 @@ export class SequencerPublisher {
|
|
|
211
305
|
}
|
|
212
306
|
}
|
|
213
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
310
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
311
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
312
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
313
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
314
|
+
*/
|
|
315
|
+
public async analyzeL1Fees(
|
|
316
|
+
l2SlotNumber: SlotNumber,
|
|
317
|
+
onComplete?: (analysis: L1FeeAnalysisResult) => void,
|
|
318
|
+
): Promise<L1FeeAnalysisResult | undefined> {
|
|
319
|
+
if (!this.l1FeeAnalyzer) {
|
|
320
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const requestsToAnalyze = [...this.requests];
|
|
325
|
+
if (requestsToAnalyze.length === 0) {
|
|
326
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Extract blob config from requests (if any)
|
|
331
|
+
const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
332
|
+
const blobConfig = blobConfigs[0];
|
|
333
|
+
|
|
334
|
+
// Get gas configs
|
|
335
|
+
const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
336
|
+
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
337
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
|
|
338
|
+
|
|
339
|
+
// Get the transaction requests
|
|
340
|
+
const l1Requests = requestsToAnalyze.map(r => r.request);
|
|
341
|
+
|
|
342
|
+
// Start the analysis
|
|
343
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
344
|
+
l2SlotNumber,
|
|
345
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
346
|
+
l1Requests,
|
|
347
|
+
blobConfig,
|
|
348
|
+
onComplete,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
this.log.info('Started L1 fee analysis', {
|
|
352
|
+
analysisId,
|
|
353
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
354
|
+
requestCount: requestsToAnalyze.length,
|
|
355
|
+
hasBlobConfig: !!blobConfig,
|
|
356
|
+
gasLimit: gasLimit.toString(),
|
|
357
|
+
actions: requestsToAnalyze.map(r => r.action),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
361
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
362
|
+
}
|
|
363
|
+
|
|
214
364
|
/**
|
|
215
365
|
* Sends all requests that are still valid.
|
|
216
366
|
* @returns one of:
|
|
@@ -218,10 +368,11 @@ export class SequencerPublisher {
|
|
|
218
368
|
* - a receipt and errorMsg if it failed on L1
|
|
219
369
|
* - undefined if no valid requests are found OR the tx failed to send.
|
|
220
370
|
*/
|
|
371
|
+
@trackSpan('SequencerPublisher.sendRequests')
|
|
221
372
|
public async sendRequests() {
|
|
222
373
|
const requestsToProcess = [...this.requests];
|
|
223
374
|
this.requests = [];
|
|
224
|
-
if (this.interrupted) {
|
|
375
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
225
376
|
return undefined;
|
|
226
377
|
}
|
|
227
378
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -253,8 +404,8 @@ export class SequencerPublisher {
|
|
|
253
404
|
// @note - we can only have one blob config per bundle
|
|
254
405
|
// find requests with gas and blob configs
|
|
255
406
|
// See https://github.com/AztecProtocol/aztec-packages/issues/11513
|
|
256
|
-
const gasConfigs =
|
|
257
|
-
const blobConfigs =
|
|
407
|
+
const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
|
|
408
|
+
const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
|
|
258
409
|
|
|
259
410
|
if (blobConfigs.length > 1) {
|
|
260
411
|
throw new Error('Multiple blob configs found');
|
|
@@ -264,7 +415,16 @@ export class SequencerPublisher {
|
|
|
264
415
|
|
|
265
416
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
266
417
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
267
|
-
|
|
418
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
419
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
420
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
421
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
422
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
423
|
+
requested: gasLimit,
|
|
424
|
+
capped: maxGas,
|
|
425
|
+
});
|
|
426
|
+
gasLimit = maxGas;
|
|
427
|
+
}
|
|
268
428
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
269
429
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
270
430
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -274,19 +434,36 @@ export class SequencerPublisher {
|
|
|
274
434
|
validRequests.sort((a, b) => compareActions(a.action, b.action));
|
|
275
435
|
|
|
276
436
|
try {
|
|
437
|
+
// Capture context for failed tx backup before sending
|
|
438
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
439
|
+
const multicallData = encodeFunctionData({
|
|
440
|
+
abi: multicall3Abi,
|
|
441
|
+
functionName: 'aggregate3',
|
|
442
|
+
args: [
|
|
443
|
+
validRequests.map(r => ({
|
|
444
|
+
target: r.request.to!,
|
|
445
|
+
callData: r.request.data!,
|
|
446
|
+
allowFailure: true,
|
|
447
|
+
})),
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
|
|
451
|
+
|
|
452
|
+
const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
|
|
453
|
+
|
|
277
454
|
this.log.debug('Forwarding transactions', {
|
|
278
455
|
validRequests: validRequests.map(request => request.action),
|
|
279
456
|
txConfig,
|
|
280
457
|
});
|
|
281
|
-
const result = await
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
458
|
+
const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
|
|
459
|
+
if (result === undefined) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
|
|
463
|
+
validRequests,
|
|
464
|
+
result,
|
|
465
|
+
txContext,
|
|
288
466
|
);
|
|
289
|
-
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
290
467
|
return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
|
|
291
468
|
} catch (err) {
|
|
292
469
|
const viemError = formatViemError(err);
|
|
@@ -304,16 +481,88 @@ export class SequencerPublisher {
|
|
|
304
481
|
}
|
|
305
482
|
}
|
|
306
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Forwards transactions via Multicall3, rotating to the next available publisher if a send
|
|
486
|
+
* failure occurs (i.e. the tx never reached the chain).
|
|
487
|
+
* On-chain reverts and simulation errors are returned as-is without rotation.
|
|
488
|
+
*/
|
|
489
|
+
private async forwardWithPublisherRotation(
|
|
490
|
+
validRequests: RequestWithExpiry[],
|
|
491
|
+
txConfig: RequestWithExpiry['gasConfig'],
|
|
492
|
+
blobConfig: L1BlobInputs | undefined,
|
|
493
|
+
) {
|
|
494
|
+
const triedAddresses: EthAddress[] = [];
|
|
495
|
+
let currentPublisher = this.l1TxUtils;
|
|
496
|
+
|
|
497
|
+
while (true) {
|
|
498
|
+
triedAddresses.push(currentPublisher.getSenderAddress());
|
|
499
|
+
try {
|
|
500
|
+
const result = await Multicall3.forward(
|
|
501
|
+
validRequests.map(r => r.request),
|
|
502
|
+
currentPublisher,
|
|
503
|
+
txConfig,
|
|
504
|
+
blobConfig,
|
|
505
|
+
this.rollupContract.address,
|
|
506
|
+
this.log,
|
|
507
|
+
);
|
|
508
|
+
this.l1TxUtils = currentPublisher;
|
|
509
|
+
return result;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
if (err instanceof TimeoutError) {
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
const viemError = formatViemError(err);
|
|
515
|
+
if (!this.getNextPublisher) {
|
|
516
|
+
this.log.error('Failed to publish bundled transactions', viemError);
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
this.log.warn(
|
|
520
|
+
`Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
|
|
521
|
+
viemError,
|
|
522
|
+
);
|
|
523
|
+
const nextPublisher = await this.getNextPublisher([...triedAddresses]);
|
|
524
|
+
if (!nextPublisher) {
|
|
525
|
+
this.log.error('All available publishers exhausted, failed to publish bundled transactions');
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
currentPublisher = nextPublisher;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
307
533
|
private callbackBundledTransactions(
|
|
308
534
|
requests: RequestWithExpiry[],
|
|
309
|
-
result
|
|
535
|
+
result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
|
|
536
|
+
txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
|
|
310
537
|
) {
|
|
311
538
|
const actionsListStr = requests.map(r => r.action).join(', ');
|
|
312
539
|
if (result instanceof FormattedViemError) {
|
|
313
540
|
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
541
|
+
this.backupFailedTx({
|
|
542
|
+
id: keccak256(txContext.multicallData),
|
|
543
|
+
failureType: 'send-error',
|
|
544
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
545
|
+
blobData: txContext.blobData,
|
|
546
|
+
l1BlockNumber: txContext.l1BlockNumber.toString(),
|
|
547
|
+
error: { message: result.message, name: result.name },
|
|
548
|
+
context: {
|
|
549
|
+
actions: requests.map(r => r.action),
|
|
550
|
+
requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
551
|
+
sender: this.getSenderAddress().toString(),
|
|
552
|
+
},
|
|
553
|
+
});
|
|
314
554
|
return { failedActions: requests.map(r => r.action) };
|
|
315
555
|
} else {
|
|
316
|
-
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
556
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
557
|
+
result,
|
|
558
|
+
requests: requests.map(r => ({
|
|
559
|
+
...r,
|
|
560
|
+
// Avoid logging large blob data
|
|
561
|
+
blobConfig: r.blobConfig
|
|
562
|
+
? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
|
|
563
|
+
: undefined,
|
|
564
|
+
})),
|
|
565
|
+
});
|
|
317
566
|
const successfulActions: Action[] = [];
|
|
318
567
|
const failedActions: Action[] = [];
|
|
319
568
|
for (const request of requests) {
|
|
@@ -323,26 +572,54 @@ export class SequencerPublisher {
|
|
|
323
572
|
failedActions.push(request.action);
|
|
324
573
|
}
|
|
325
574
|
}
|
|
575
|
+
// Single backup for the whole reverted tx
|
|
576
|
+
if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
|
|
577
|
+
this.backupFailedTx({
|
|
578
|
+
id: result.receipt.transactionHash,
|
|
579
|
+
failureType: 'revert',
|
|
580
|
+
request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
|
|
581
|
+
blobData: txContext.blobData,
|
|
582
|
+
l1BlockNumber: result.receipt.blockNumber.toString(),
|
|
583
|
+
receipt: {
|
|
584
|
+
transactionHash: result.receipt.transactionHash,
|
|
585
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
586
|
+
gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
|
|
587
|
+
status: 'reverted',
|
|
588
|
+
},
|
|
589
|
+
error: { message: result.errorMsg ?? 'Transaction reverted' },
|
|
590
|
+
context: {
|
|
591
|
+
actions: failedActions,
|
|
592
|
+
requests: requests
|
|
593
|
+
.filter(r => failedActions.includes(r.action))
|
|
594
|
+
.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
|
|
595
|
+
sender: this.getSenderAddress().toString(),
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
326
599
|
return { successfulActions, failedActions };
|
|
327
600
|
}
|
|
328
601
|
}
|
|
329
602
|
|
|
330
603
|
/**
|
|
331
|
-
* @notice Will call `
|
|
604
|
+
* @notice Will call `canProposeAt` to make sure that it is possible to propose
|
|
332
605
|
* @param tipArchive - The archive to check
|
|
333
606
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
334
607
|
*/
|
|
335
|
-
public
|
|
608
|
+
public async canProposeAt(
|
|
336
609
|
tipArchive: Fr,
|
|
337
610
|
msgSender: EthAddress,
|
|
338
|
-
opts: {
|
|
611
|
+
opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
|
|
339
612
|
) {
|
|
340
613
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
341
614
|
const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
|
|
342
615
|
|
|
616
|
+
const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
|
|
617
|
+
const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
|
|
618
|
+
const nextL1SlotTs = (await this.getNextL1SlotTimestampWithL1Floor()) + slotOffset;
|
|
619
|
+
|
|
343
620
|
return this.rollupContract
|
|
344
|
-
.
|
|
345
|
-
forcePendingCheckpointNumber: opts.
|
|
621
|
+
.canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
|
|
622
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
346
623
|
})
|
|
347
624
|
.catch(err => {
|
|
348
625
|
if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
|
|
@@ -355,13 +632,18 @@ export class SequencerPublisher {
|
|
|
355
632
|
return undefined;
|
|
356
633
|
});
|
|
357
634
|
}
|
|
635
|
+
|
|
358
636
|
/**
|
|
359
637
|
* @notice Will simulate `validateHeader` to make sure that the block header is valid
|
|
360
638
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
361
639
|
* It will throw if the block header is invalid.
|
|
362
640
|
* @param header - The block header to validate
|
|
363
641
|
*/
|
|
364
|
-
|
|
642
|
+
@trackSpan('SequencerPublisher.validateBlockHeader')
|
|
643
|
+
public async validateBlockHeader(
|
|
644
|
+
header: CheckpointHeader,
|
|
645
|
+
opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
|
|
646
|
+
): Promise<void> {
|
|
365
647
|
const flags = { ignoreDA: true, ignoreSignatures: true };
|
|
366
648
|
|
|
367
649
|
const args = [
|
|
@@ -370,12 +652,14 @@ export class SequencerPublisher {
|
|
|
370
652
|
[], // no signers
|
|
371
653
|
Signature.empty().toViemSignature(),
|
|
372
654
|
`0x${'0'.repeat(64)}`, // 32 empty bytes
|
|
373
|
-
header.
|
|
655
|
+
header.blobsHash.toString(),
|
|
374
656
|
flags,
|
|
375
657
|
] as const;
|
|
376
658
|
|
|
377
|
-
const ts =
|
|
378
|
-
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
659
|
+
const ts = await this.getNextL1SlotTimestampWithL1Floor();
|
|
660
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
|
|
661
|
+
opts?.forcePendingCheckpointNumber,
|
|
662
|
+
);
|
|
379
663
|
let balance = 0n;
|
|
380
664
|
if (this.config.fishermanMode) {
|
|
381
665
|
// In fisherman mode, we can't know where the proposer is publishing from
|
|
@@ -402,77 +686,109 @@ export class SequencerPublisher {
|
|
|
402
686
|
}
|
|
403
687
|
|
|
404
688
|
/**
|
|
405
|
-
* Simulate making a call to invalidate a
|
|
406
|
-
* @param
|
|
689
|
+
* Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
|
|
690
|
+
* @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
|
|
407
691
|
*/
|
|
408
|
-
public async
|
|
409
|
-
validationResult:
|
|
410
|
-
): Promise<
|
|
692
|
+
public async simulateInvalidateCheckpoint(
|
|
693
|
+
validationResult: ValidateCheckpointResult,
|
|
694
|
+
): Promise<InvalidateCheckpointRequest | undefined> {
|
|
411
695
|
if (validationResult.valid) {
|
|
412
696
|
return undefined;
|
|
413
697
|
}
|
|
414
698
|
|
|
415
|
-
const { reason,
|
|
416
|
-
const
|
|
417
|
-
const logData = { ...
|
|
699
|
+
const { reason, checkpoint } = validationResult;
|
|
700
|
+
const checkpointNumber = checkpoint.checkpointNumber;
|
|
701
|
+
const logData = { ...checkpoint, reason };
|
|
418
702
|
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
703
|
+
const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
704
|
+
if (currentCheckpointNumber < checkpointNumber) {
|
|
421
705
|
this.log.verbose(
|
|
422
|
-
`Skipping
|
|
423
|
-
{
|
|
706
|
+
`Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
|
|
707
|
+
{ currentCheckpointNumber, ...logData },
|
|
424
708
|
);
|
|
425
709
|
return undefined;
|
|
426
710
|
}
|
|
427
711
|
|
|
428
|
-
const request = this.
|
|
429
|
-
this.log.debug(`Simulating invalidate
|
|
712
|
+
const request = this.buildInvalidateCheckpointRequest(validationResult);
|
|
713
|
+
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
714
|
+
|
|
715
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
430
716
|
|
|
431
717
|
try {
|
|
432
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
433
|
-
|
|
718
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
719
|
+
request,
|
|
720
|
+
undefined,
|
|
721
|
+
undefined,
|
|
722
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
723
|
+
);
|
|
724
|
+
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
725
|
+
...logData,
|
|
726
|
+
request,
|
|
727
|
+
gasUsed,
|
|
728
|
+
});
|
|
434
729
|
|
|
435
|
-
return {
|
|
730
|
+
return {
|
|
731
|
+
request,
|
|
732
|
+
gasUsed,
|
|
733
|
+
checkpointNumber,
|
|
734
|
+
forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
|
|
735
|
+
reason,
|
|
736
|
+
};
|
|
436
737
|
} catch (err) {
|
|
437
738
|
const viemError = formatViemError(err);
|
|
438
739
|
|
|
439
|
-
// If the error is due to the
|
|
440
|
-
// we can safely ignore it and return undefined so we go ahead with
|
|
441
|
-
if (viemError.message?.includes('
|
|
740
|
+
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
741
|
+
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
742
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
442
743
|
this.log.verbose(
|
|
443
|
-
`Simulation for invalidate
|
|
744
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
444
745
|
{ ...logData, request, error: viemError.message },
|
|
445
746
|
);
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
this.log.verbose(`
|
|
747
|
+
const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
|
|
748
|
+
if (latestPendingCheckpointNumber < checkpointNumber) {
|
|
749
|
+
this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
|
|
449
750
|
return undefined;
|
|
450
751
|
} else {
|
|
451
752
|
this.log.error(
|
|
452
|
-
`Simulation for invalidate ${
|
|
753
|
+
`Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
|
|
453
754
|
viemError,
|
|
454
755
|
logData,
|
|
455
756
|
);
|
|
456
|
-
throw new Error(
|
|
457
|
-
|
|
458
|
-
|
|
757
|
+
throw new Error(
|
|
758
|
+
`Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
|
|
759
|
+
{
|
|
760
|
+
cause: viemError,
|
|
761
|
+
},
|
|
762
|
+
);
|
|
459
763
|
}
|
|
460
764
|
}
|
|
461
765
|
|
|
462
|
-
// Otherwise, throw. We cannot build the next
|
|
463
|
-
this.log.error(`Simulation for invalidate
|
|
464
|
-
|
|
766
|
+
// Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
|
|
767
|
+
this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
|
|
768
|
+
this.backupFailedTx({
|
|
769
|
+
id: keccak256(request.data!),
|
|
770
|
+
failureType: 'simulation',
|
|
771
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
772
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
773
|
+
error: { message: viemError.message, name: viemError.name },
|
|
774
|
+
context: {
|
|
775
|
+
actions: [`invalidate-${reason}`],
|
|
776
|
+
checkpointNumber,
|
|
777
|
+
sender: this.getSenderAddress().toString(),
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
|
|
465
781
|
}
|
|
466
782
|
}
|
|
467
783
|
|
|
468
|
-
private
|
|
784
|
+
private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
|
|
469
785
|
if (validationResult.valid) {
|
|
470
|
-
throw new Error('Cannot invalidate a valid
|
|
786
|
+
throw new Error('Cannot invalidate a valid checkpoint');
|
|
471
787
|
}
|
|
472
788
|
|
|
473
|
-
const {
|
|
474
|
-
const logData = { ...
|
|
475
|
-
this.log.debug(`
|
|
789
|
+
const { checkpoint, committee, reason } = validationResult;
|
|
790
|
+
const logData = { ...checkpoint, reason };
|
|
791
|
+
this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
|
|
476
792
|
|
|
477
793
|
const attestationsAndSigners = new CommitteeAttestationsAndSigners(
|
|
478
794
|
validationResult.attestations,
|
|
@@ -480,14 +796,14 @@ export class SequencerPublisher {
|
|
|
480
796
|
|
|
481
797
|
if (reason === 'invalid-attestation') {
|
|
482
798
|
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
483
|
-
|
|
799
|
+
checkpoint.checkpointNumber,
|
|
484
800
|
attestationsAndSigners,
|
|
485
801
|
committee,
|
|
486
802
|
validationResult.invalidIndex,
|
|
487
803
|
);
|
|
488
804
|
} else if (reason === 'insufficient-attestations') {
|
|
489
805
|
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
490
|
-
|
|
806
|
+
checkpoint.checkpointNumber,
|
|
491
807
|
attestationsAndSigners,
|
|
492
808
|
committee,
|
|
493
809
|
);
|
|
@@ -497,47 +813,27 @@ export class SequencerPublisher {
|
|
|
497
813
|
}
|
|
498
814
|
}
|
|
499
815
|
|
|
500
|
-
/**
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
*
|
|
505
|
-
* @param block - The block to propose
|
|
506
|
-
* @param attestationData - The block's attestation data
|
|
507
|
-
*
|
|
508
|
-
*/
|
|
509
|
-
public async validateBlockForSubmission(
|
|
510
|
-
block: L2Block,
|
|
816
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */
|
|
817
|
+
@trackSpan('SequencerPublisher.validateCheckpointForSubmission')
|
|
818
|
+
public async validateCheckpointForSubmission(
|
|
819
|
+
checkpoint: Checkpoint,
|
|
511
820
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
512
821
|
attestationsAndSignersSignature: Signature,
|
|
513
|
-
options: {
|
|
822
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
514
823
|
): Promise<bigint> {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
if (ignoreSignatures) {
|
|
521
|
-
const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
522
|
-
if (!committee) {
|
|
523
|
-
this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
524
|
-
throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
525
|
-
}
|
|
526
|
-
attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
527
|
-
CommitteeAttestation.fromAddress(committeeMember),
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const blobFields = block.getCheckpointBlobFields();
|
|
532
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
824
|
+
// Anchor the simulation timestamp to the checkpoint's own slot start time
|
|
825
|
+
// rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
|
|
826
|
+
const ts = checkpoint.header.timestamp;
|
|
827
|
+
const blobFields = checkpoint.toBlobFields();
|
|
828
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
533
829
|
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
534
830
|
|
|
535
831
|
const args = [
|
|
536
832
|
{
|
|
537
|
-
header:
|
|
538
|
-
archive: toHex(
|
|
833
|
+
header: checkpoint.header.toViem(),
|
|
834
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
539
835
|
oracleInput: {
|
|
540
|
-
feeAssetPriceModifier:
|
|
836
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
541
837
|
},
|
|
542
838
|
},
|
|
543
839
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -573,10 +869,45 @@ export class SequencerPublisher {
|
|
|
573
869
|
const round = await base.computeRound(slotNumber);
|
|
574
870
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
575
871
|
|
|
872
|
+
if (roundInfo.quorumReached) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
576
876
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
577
877
|
return false;
|
|
578
878
|
}
|
|
579
879
|
|
|
880
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
881
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Check if payload was already submitted to governance
|
|
886
|
+
const cacheKey = payload.toString();
|
|
887
|
+
if (!this.payloadProposedCache.has(cacheKey)) {
|
|
888
|
+
try {
|
|
889
|
+
const l1StartBlock = await this.rollupContract.getL1StartBlock();
|
|
890
|
+
const proposed = await retry(
|
|
891
|
+
() => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
|
|
892
|
+
'Check if payload was proposed',
|
|
893
|
+
makeBackoff([0, 1, 2]),
|
|
894
|
+
this.log,
|
|
895
|
+
true,
|
|
896
|
+
);
|
|
897
|
+
if (proposed) {
|
|
898
|
+
this.payloadProposedCache.add(cacheKey);
|
|
899
|
+
}
|
|
900
|
+
} catch (err) {
|
|
901
|
+
this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (this.payloadProposedCache.has(cacheKey)) {
|
|
907
|
+
this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
|
|
580
911
|
const cachedLastVote = this.lastActions[signalType];
|
|
581
912
|
this.lastActions[signalType] = slotNumber;
|
|
582
913
|
const action = signalType;
|
|
@@ -595,11 +926,26 @@ export class SequencerPublisher {
|
|
|
595
926
|
lastValidL2Slot: slotNumber,
|
|
596
927
|
});
|
|
597
928
|
|
|
929
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
930
|
+
|
|
598
931
|
try {
|
|
599
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
932
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
600
933
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
601
934
|
} catch (err) {
|
|
602
|
-
|
|
935
|
+
const viemError = formatViemError(err);
|
|
936
|
+
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
|
|
937
|
+
this.backupFailedTx({
|
|
938
|
+
id: keccak256(request.data!),
|
|
939
|
+
failureType: 'simulation',
|
|
940
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
941
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
942
|
+
error: { message: viemError.message, name: viemError.name },
|
|
943
|
+
context: {
|
|
944
|
+
actions: [action],
|
|
945
|
+
slot: slotNumber,
|
|
946
|
+
sender: this.getSenderAddress().toString(),
|
|
947
|
+
},
|
|
948
|
+
});
|
|
603
949
|
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
604
950
|
}
|
|
605
951
|
|
|
@@ -619,14 +965,14 @@ export class SequencerPublisher {
|
|
|
619
965
|
const logData = { ...result, slotNumber, round, payload: payload.toString() };
|
|
620
966
|
if (!success) {
|
|
621
967
|
this.log.error(
|
|
622
|
-
`Signaling in
|
|
968
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
|
|
623
969
|
logData,
|
|
624
970
|
);
|
|
625
971
|
this.lastActions[signalType] = cachedLastVote;
|
|
626
972
|
return false;
|
|
627
973
|
} else {
|
|
628
974
|
this.log.info(
|
|
629
|
-
`Signaling in
|
|
975
|
+
`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
|
|
630
976
|
logData,
|
|
631
977
|
);
|
|
632
978
|
return true;
|
|
@@ -636,6 +982,17 @@ export class SequencerPublisher {
|
|
|
636
982
|
return true;
|
|
637
983
|
}
|
|
638
984
|
|
|
985
|
+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
|
|
986
|
+
const key = payload.toString();
|
|
987
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
988
|
+
if (cached) {
|
|
989
|
+
return cached;
|
|
990
|
+
}
|
|
991
|
+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
|
|
992
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
993
|
+
return isEmpty;
|
|
994
|
+
}
|
|
995
|
+
|
|
639
996
|
/**
|
|
640
997
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
641
998
|
* @param slotNumber - The slot number to cast a signal for.
|
|
@@ -783,30 +1140,25 @@ export class SequencerPublisher {
|
|
|
783
1140
|
return true;
|
|
784
1141
|
}
|
|
785
1142
|
|
|
786
|
-
/**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
* @param block - L2 block to propose.
|
|
790
|
-
* @returns True if the tx has been enqueued, throws otherwise. See #9315
|
|
791
|
-
*/
|
|
792
|
-
public async enqueueProposeL2Block(
|
|
793
|
-
block: L2Block,
|
|
1143
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */
|
|
1144
|
+
public async enqueueProposeCheckpoint(
|
|
1145
|
+
checkpoint: Checkpoint,
|
|
794
1146
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
795
1147
|
attestationsAndSignersSignature: Signature,
|
|
796
|
-
opts: { txTimeoutAt?: Date;
|
|
797
|
-
): Promise<
|
|
798
|
-
const checkpointHeader =
|
|
1148
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1149
|
+
): Promise<void> {
|
|
1150
|
+
const checkpointHeader = checkpoint.header;
|
|
799
1151
|
|
|
800
|
-
const blobFields =
|
|
801
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
1152
|
+
const blobFields = checkpoint.toBlobFields();
|
|
1153
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
802
1154
|
|
|
803
|
-
const proposeTxArgs = {
|
|
1155
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
804
1156
|
header: checkpointHeader,
|
|
805
|
-
archive:
|
|
806
|
-
body: block.body.toBuffer(),
|
|
1157
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
807
1158
|
blobs,
|
|
808
1159
|
attestationsAndSigners,
|
|
809
1160
|
attestationsAndSignersSignature,
|
|
1161
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
810
1162
|
};
|
|
811
1163
|
|
|
812
1164
|
let ts: bigint;
|
|
@@ -817,22 +1169,29 @@ export class SequencerPublisher {
|
|
|
817
1169
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
818
1170
|
// make time consistency checks break.
|
|
819
1171
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
820
|
-
ts = await this.
|
|
1172
|
+
ts = await this.validateCheckpointForSubmission(
|
|
1173
|
+
checkpoint,
|
|
1174
|
+
attestationsAndSigners,
|
|
1175
|
+
attestationsAndSignersSignature,
|
|
1176
|
+
opts,
|
|
1177
|
+
);
|
|
821
1178
|
} catch (err: any) {
|
|
822
|
-
this.log.error(`
|
|
823
|
-
...
|
|
824
|
-
slotNumber:
|
|
825
|
-
|
|
1179
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
1180
|
+
...checkpoint.getStats(),
|
|
1181
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
1182
|
+
forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
|
|
826
1183
|
});
|
|
827
1184
|
throw err;
|
|
828
1185
|
}
|
|
829
1186
|
|
|
830
|
-
this.log.verbose(`Enqueuing
|
|
831
|
-
await this.addProposeTx(
|
|
832
|
-
return true;
|
|
1187
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
|
|
1188
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
833
1189
|
}
|
|
834
1190
|
|
|
835
|
-
public
|
|
1191
|
+
public enqueueInvalidateCheckpoint(
|
|
1192
|
+
request: InvalidateCheckpointRequest | undefined,
|
|
1193
|
+
opts: { txTimeoutAt?: Date } = {},
|
|
1194
|
+
) {
|
|
836
1195
|
if (!request) {
|
|
837
1196
|
return;
|
|
838
1197
|
}
|
|
@@ -840,9 +1199,9 @@ export class SequencerPublisher {
|
|
|
840
1199
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
841
1200
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
|
|
842
1201
|
|
|
843
|
-
const { gasUsed,
|
|
844
|
-
const logData = { gasUsed,
|
|
845
|
-
this.log.verbose(`Enqueuing invalidate
|
|
1202
|
+
const { gasUsed, checkpointNumber } = request;
|
|
1203
|
+
const logData = { gasUsed, checkpointNumber, gasLimit, opts };
|
|
1204
|
+
this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
|
|
846
1205
|
this.addRequest({
|
|
847
1206
|
action: `invalidate-by-${request.reason}`,
|
|
848
1207
|
request: request.request,
|
|
@@ -855,9 +1214,9 @@ export class SequencerPublisher {
|
|
|
855
1214
|
result.receipt.status === 'success' &&
|
|
856
1215
|
tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
857
1216
|
if (!success) {
|
|
858
|
-
this.log.warn(`Invalidate
|
|
1217
|
+
this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
|
|
859
1218
|
} else {
|
|
860
|
-
this.log.info(`Invalidate
|
|
1219
|
+
this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
|
|
861
1220
|
}
|
|
862
1221
|
return !!success;
|
|
863
1222
|
},
|
|
@@ -882,13 +1241,30 @@ export class SequencerPublisher {
|
|
|
882
1241
|
|
|
883
1242
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
884
1243
|
|
|
1244
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1245
|
+
|
|
885
1246
|
let gasUsed: bigint;
|
|
1247
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
886
1248
|
try {
|
|
887
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1249
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
888
1250
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
889
1251
|
} catch (err) {
|
|
890
|
-
const viemError = formatViemError(err);
|
|
1252
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
891
1253
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1254
|
+
|
|
1255
|
+
this.backupFailedTx({
|
|
1256
|
+
id: keccak256(request.data!),
|
|
1257
|
+
failureType: 'simulation',
|
|
1258
|
+
request: { to: request.to!, data: request.data!, value: request.value?.toString() },
|
|
1259
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1260
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1261
|
+
context: {
|
|
1262
|
+
actions: [action],
|
|
1263
|
+
slot: slotNumber,
|
|
1264
|
+
sender: this.getSenderAddress().toString(),
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
|
|
892
1268
|
return false;
|
|
893
1269
|
}
|
|
894
1270
|
|
|
@@ -896,10 +1272,14 @@ export class SequencerPublisher {
|
|
|
896
1272
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
897
1273
|
logData.gasLimit = gasLimit;
|
|
898
1274
|
|
|
1275
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1276
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1277
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1278
|
+
|
|
899
1279
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
900
1280
|
this.addRequest({
|
|
901
1281
|
action,
|
|
902
|
-
request,
|
|
1282
|
+
request: requestWithAbi,
|
|
903
1283
|
gasConfig: { gasLimit },
|
|
904
1284
|
lastValidL2Slot: slotNumber,
|
|
905
1285
|
checkSuccess: (_req, result) => {
|
|
@@ -936,7 +1316,7 @@ export class SequencerPublisher {
|
|
|
936
1316
|
private async prepareProposeTx(
|
|
937
1317
|
encodedData: L1ProcessArgs,
|
|
938
1318
|
timestamp: bigint,
|
|
939
|
-
options: {
|
|
1319
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
940
1320
|
) {
|
|
941
1321
|
const kzg = Blob.getViemKzgInstance();
|
|
942
1322
|
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
@@ -968,9 +1348,27 @@ export class SequencerPublisher {
|
|
|
968
1348
|
kzg,
|
|
969
1349
|
},
|
|
970
1350
|
)
|
|
971
|
-
.catch(err => {
|
|
972
|
-
const
|
|
973
|
-
this.log.error(`Failed to validate blobs`, message, { metaMessages });
|
|
1351
|
+
.catch(async err => {
|
|
1352
|
+
const viemError = formatViemError(err);
|
|
1353
|
+
this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
|
|
1354
|
+
const validateBlobsData = encodeFunctionData({
|
|
1355
|
+
abi: RollupAbi,
|
|
1356
|
+
functionName: 'validateBlobs',
|
|
1357
|
+
args: [blobInput],
|
|
1358
|
+
});
|
|
1359
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1360
|
+
this.backupFailedTx({
|
|
1361
|
+
id: keccak256(validateBlobsData),
|
|
1362
|
+
failureType: 'simulation',
|
|
1363
|
+
request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
|
|
1364
|
+
blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
|
|
1365
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1366
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1367
|
+
context: {
|
|
1368
|
+
actions: ['validate-blobs'],
|
|
1369
|
+
sender: this.getSenderAddress().toString(),
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
974
1372
|
throw new Error('Failed to validate blobs');
|
|
975
1373
|
});
|
|
976
1374
|
}
|
|
@@ -981,8 +1379,7 @@ export class SequencerPublisher {
|
|
|
981
1379
|
header: encodedData.header.toViem(),
|
|
982
1380
|
archive: toHex(encodedData.archive),
|
|
983
1381
|
oracleInput: {
|
|
984
|
-
|
|
985
|
-
feeAssetPriceModifier: 0n,
|
|
1382
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
986
1383
|
},
|
|
987
1384
|
},
|
|
988
1385
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1008,7 +1405,7 @@ export class SequencerPublisher {
|
|
|
1008
1405
|
readonly header: ViemHeader;
|
|
1009
1406
|
readonly archive: `0x${string}`;
|
|
1010
1407
|
readonly oracleInput: {
|
|
1011
|
-
readonly feeAssetPriceModifier:
|
|
1408
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1012
1409
|
};
|
|
1013
1410
|
},
|
|
1014
1411
|
ViemCommitteeAttestations,
|
|
@@ -1017,7 +1414,7 @@ export class SequencerPublisher {
|
|
|
1017
1414
|
`0x${string}`,
|
|
1018
1415
|
],
|
|
1019
1416
|
timestamp: bigint,
|
|
1020
|
-
options: {
|
|
1417
|
+
options: { forcePendingCheckpointNumber?: CheckpointNumber },
|
|
1021
1418
|
) {
|
|
1022
1419
|
const rollupData = encodeFunctionData({
|
|
1023
1420
|
abi: RollupAbi,
|
|
@@ -1025,10 +1422,10 @@ export class SequencerPublisher {
|
|
|
1025
1422
|
args,
|
|
1026
1423
|
});
|
|
1027
1424
|
|
|
1028
|
-
// override the pending
|
|
1029
|
-
const
|
|
1030
|
-
options.
|
|
1031
|
-
? await this.rollupContract.makePendingCheckpointNumberOverride(options.
|
|
1425
|
+
// override the pending checkpoint number if requested
|
|
1426
|
+
const forcePendingCheckpointNumberStateDiff = (
|
|
1427
|
+
options.forcePendingCheckpointNumber !== undefined
|
|
1428
|
+
? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
|
|
1032
1429
|
: []
|
|
1033
1430
|
).flatMap(override => override.stateDiff ?? []);
|
|
1034
1431
|
|
|
@@ -1038,7 +1435,7 @@ export class SequencerPublisher {
|
|
|
1038
1435
|
// @note we override checkBlob to false since blobs are not part simulate()
|
|
1039
1436
|
stateDiff: [
|
|
1040
1437
|
{ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
|
|
1041
|
-
...
|
|
1438
|
+
...forcePendingCheckpointNumberStateDiff,
|
|
1042
1439
|
],
|
|
1043
1440
|
},
|
|
1044
1441
|
];
|
|
@@ -1050,25 +1447,27 @@ export class SequencerPublisher {
|
|
|
1050
1447
|
});
|
|
1051
1448
|
}
|
|
1052
1449
|
|
|
1450
|
+
const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
|
|
1451
|
+
|
|
1053
1452
|
const simulationResult = await this.l1TxUtils
|
|
1054
1453
|
.simulate(
|
|
1055
1454
|
{
|
|
1056
1455
|
to: this.rollupContract.address,
|
|
1057
1456
|
data: rollupData,
|
|
1058
|
-
gas:
|
|
1457
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1059
1458
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1060
1459
|
},
|
|
1061
1460
|
{
|
|
1062
1461
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1063
1462
|
time: timestamp + 1n,
|
|
1064
1463
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1065
|
-
gasLimit:
|
|
1464
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1066
1465
|
},
|
|
1067
1466
|
stateOverrides,
|
|
1068
1467
|
RollupAbi,
|
|
1069
1468
|
{
|
|
1070
1469
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1071
|
-
fallbackGasEstimate:
|
|
1470
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1072
1471
|
},
|
|
1073
1472
|
)
|
|
1074
1473
|
.catch(err => {
|
|
@@ -1078,11 +1477,23 @@ export class SequencerPublisher {
|
|
|
1078
1477
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1079
1478
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1080
1479
|
return {
|
|
1081
|
-
gasUsed:
|
|
1480
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1082
1481
|
logs: [],
|
|
1083
1482
|
};
|
|
1084
1483
|
}
|
|
1085
1484
|
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
1485
|
+
this.backupFailedTx({
|
|
1486
|
+
id: keccak256(rollupData),
|
|
1487
|
+
failureType: 'simulation',
|
|
1488
|
+
request: { to: this.rollupContract.address, data: rollupData },
|
|
1489
|
+
l1BlockNumber: l1BlockNumber.toString(),
|
|
1490
|
+
error: { message: viemError.message, name: viemError.name },
|
|
1491
|
+
context: {
|
|
1492
|
+
actions: ['propose'],
|
|
1493
|
+
slot: Number(args[0].header.slotNumber),
|
|
1494
|
+
sender: this.getSenderAddress().toString(),
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1086
1497
|
throw err;
|
|
1087
1498
|
});
|
|
1088
1499
|
|
|
@@ -1090,11 +1501,12 @@ export class SequencerPublisher {
|
|
|
1090
1501
|
}
|
|
1091
1502
|
|
|
1092
1503
|
private async addProposeTx(
|
|
1093
|
-
|
|
1504
|
+
checkpoint: Checkpoint,
|
|
1094
1505
|
encodedData: L1ProcessArgs,
|
|
1095
|
-
opts: { txTimeoutAt?: Date;
|
|
1506
|
+
opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
|
|
1096
1507
|
timestamp: bigint,
|
|
1097
1508
|
): Promise<void> {
|
|
1509
|
+
const slot = checkpoint.header.slotNumber;
|
|
1098
1510
|
const timer = new Timer();
|
|
1099
1511
|
const kzg = Blob.getViemKzgInstance();
|
|
1100
1512
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
|
|
@@ -1109,11 +1521,13 @@ export class SequencerPublisher {
|
|
|
1109
1521
|
SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
1110
1522
|
);
|
|
1111
1523
|
|
|
1112
|
-
// Send the blobs to the blob
|
|
1113
|
-
// tx fails but it does get mined. We make sure that the blobs are sent to the blob
|
|
1114
|
-
void
|
|
1115
|
-
this.
|
|
1116
|
-
|
|
1524
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
1525
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
1526
|
+
void Promise.resolve().then(() =>
|
|
1527
|
+
this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
|
|
1528
|
+
this.log.error('Failed to send blobs to blob client');
|
|
1529
|
+
}),
|
|
1530
|
+
);
|
|
1117
1531
|
|
|
1118
1532
|
return this.addRequest({
|
|
1119
1533
|
action: 'propose',
|
|
@@ -1121,7 +1535,7 @@ export class SequencerPublisher {
|
|
|
1121
1535
|
to: this.rollupContract.address,
|
|
1122
1536
|
data: rollupData,
|
|
1123
1537
|
},
|
|
1124
|
-
lastValidL2Slot:
|
|
1538
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
1125
1539
|
gasConfig: { ...opts, gasLimit },
|
|
1126
1540
|
blobConfig: {
|
|
1127
1541
|
blobs: encodedData.blobs.map(b => b.data),
|
|
@@ -1136,11 +1550,12 @@ export class SequencerPublisher {
|
|
|
1136
1550
|
receipt &&
|
|
1137
1551
|
receipt.status === 'success' &&
|
|
1138
1552
|
tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
1553
|
+
|
|
1139
1554
|
if (success) {
|
|
1140
1555
|
const endBlock = receipt.blockNumber;
|
|
1141
1556
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
1142
1557
|
const { calldataGas, calldataSize, sender } = stats!;
|
|
1143
|
-
const publishStats:
|
|
1558
|
+
const publishStats: L1PublishCheckpointStats = {
|
|
1144
1559
|
gasPrice: receipt.effectiveGasPrice,
|
|
1145
1560
|
gasUsed: receipt.gasUsed,
|
|
1146
1561
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
@@ -1149,26 +1564,46 @@ export class SequencerPublisher {
|
|
|
1149
1564
|
calldataGas,
|
|
1150
1565
|
calldataSize,
|
|
1151
1566
|
sender,
|
|
1152
|
-
...
|
|
1567
|
+
...checkpoint.getStats(),
|
|
1153
1568
|
eventName: 'rollup-published-to-l1',
|
|
1154
1569
|
blobCount: encodedData.blobs.length,
|
|
1155
1570
|
inclusionBlocks,
|
|
1156
1571
|
};
|
|
1157
|
-
this.log.info(`Published
|
|
1572
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
1573
|
+
...stats,
|
|
1574
|
+
...checkpoint.getStats(),
|
|
1575
|
+
...pick(receipt, 'transactionHash', 'blockHash'),
|
|
1576
|
+
});
|
|
1158
1577
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
1159
1578
|
|
|
1160
1579
|
return true;
|
|
1161
1580
|
} else {
|
|
1162
1581
|
this.metrics.recordFailedTx('process');
|
|
1163
|
-
this.log.error(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
});
|
|
1582
|
+
this.log.error(
|
|
1583
|
+
`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
|
|
1584
|
+
undefined,
|
|
1585
|
+
{ ...checkpoint.getStats(), ...receipt },
|
|
1586
|
+
);
|
|
1169
1587
|
return false;
|
|
1170
1588
|
}
|
|
1171
1589
|
},
|
|
1172
1590
|
});
|
|
1173
1591
|
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Returns the timestamp to use when simulating L1 proposal calls.
|
|
1595
|
+
* Uses the wall-clock-based next L1 slot boundary, but floors it with the latest L1 block timestamp
|
|
1596
|
+
* plus one slot duration. This prevents the sequencer from targeting a future L2 slot when the L1
|
|
1597
|
+
* chain hasn't caught up to the wall clock yet (e.g., the dateProvider is one L1 slot ahead of the
|
|
1598
|
+
* latest mined block), which would cause the propose tx to land in an L1 block with block.timestamp
|
|
1599
|
+
* still in the previous L2 slot.
|
|
1600
|
+
* TODO(palla): Properly fix by keeping dateProvider synced with anvil's chain time on every block.
|
|
1601
|
+
*/
|
|
1602
|
+
private async getNextL1SlotTimestampWithL1Floor(): Promise<bigint> {
|
|
1603
|
+
const l1Constants = this.epochCache.getL1Constants();
|
|
1604
|
+
const fromWallClock = getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
|
|
1605
|
+
const latestBlock = await this.l1TxUtils.client.getBlock();
|
|
1606
|
+
const fromL1Block = latestBlock.timestamp + BigInt(l1Constants.ethereumSlotDuration);
|
|
1607
|
+
return fromWallClock > fromL1Block ? fromWallClock : fromL1Block;
|
|
1608
|
+
}
|
|
1174
1609
|
}
|