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