@aztec/sequencer-client 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107
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/index.d.ts +1 -1
- package/dest/client/sequencer-client.d.ts +10 -8
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +40 -28
- package/dest/config.d.ts +13 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +82 -25
- package/dest/global_variable_builder/global_builder.d.ts +19 -13
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +41 -28
- package/dest/global_variable_builder/index.d.ts +1 -1
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +11 -8
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +21 -13
- package/dest/publisher/index.d.ts +2 -2
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +9 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts +4 -4
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +76 -69
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +290 -180
- package/dest/sequencer/block_builder.d.ts +6 -10
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +21 -10
- package/dest/sequencer/checkpoint_builder.d.ts +63 -0
- package/dest/sequencer/checkpoint_builder.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_builder.js +131 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +74 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job.js +642 -0
- package/dest/sequencer/checkpoint_voter.d.ts +34 -0
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_voter.js +85 -0
- package/dest/sequencer/config.d.ts +3 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +11 -0
- package/dest/sequencer/errors.d.ts.map +1 -0
- package/dest/sequencer/errors.js +15 -0
- package/dest/sequencer/events.d.ts +46 -0
- package/dest/sequencer/events.d.ts.map +1 -0
- package/dest/sequencer/events.js +1 -0
- package/dest/sequencer/index.d.ts +5 -1
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +4 -0
- package/dest/sequencer/metrics.d.ts +37 -20
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +211 -85
- package/dest/sequencer/sequencer.d.ts +109 -121
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +798 -525
- package/dest/sequencer/timetable.d.ts +57 -21
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +150 -68
- 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 +20 -28
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +12 -24
- package/dest/test/index.d.ts +4 -2
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +83 -0
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
- package/dest/test/mock_checkpoint_builder.js +179 -0
- package/dest/test/utils.d.ts +49 -0
- package/dest/test/utils.d.ts.map +1 -0
- package/dest/test/utils.js +94 -0
- package/dest/tx_validator/nullifier_cache.d.ts +1 -1
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +12 -9
- package/package.json +32 -31
- package/src/client/sequencer-client.ts +34 -40
- package/src/config.ts +89 -29
- package/src/global_variable_builder/global_builder.ts +56 -48
- package/src/index.ts +2 -0
- package/src/publisher/config.ts +32 -19
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher-factory.ts +19 -6
- package/src/publisher/sequencer-publisher-metrics.ts +3 -3
- package/src/publisher/sequencer-publisher.ts +410 -240
- package/src/sequencer/README.md +531 -0
- package/src/sequencer/block_builder.ts +28 -30
- package/src/sequencer/checkpoint_builder.ts +217 -0
- package/src/sequencer/checkpoint_proposal_job.ts +706 -0
- package/src/sequencer/checkpoint_voter.ts +105 -0
- package/src/sequencer/config.ts +2 -1
- package/src/sequencer/errors.ts +21 -0
- package/src/sequencer/events.ts +27 -0
- package/src/sequencer/index.ts +4 -0
- package/src/sequencer/metrics.ts +269 -94
- package/src/sequencer/sequencer.ts +506 -676
- package/src/sequencer/timetable.ts +181 -91
- package/src/sequencer/types.ts +6 -0
- package/src/sequencer/utils.ts +24 -29
- package/src/test/index.ts +3 -1
- package/src/test/mock_checkpoint_builder.ts +247 -0
- package/src/test/utils.ts +137 -0
- package/src/tx_validator/tx_validator_factory.ts +13 -7
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
import { Blob } from '@aztec/blob-lib';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
|
|
2
|
+
import { MULTI_CALL_3_ADDRESS, Multicall3, RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
|
|
4
|
+
import { WEI_CONST } from '@aztec/ethereum/l1-tx-utils';
|
|
5
|
+
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
4
6
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
5
7
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
8
|
+
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
9
|
+
import { pick } from '@aztec/foundation/collection';
|
|
6
10
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
11
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
7
12
|
import { createLogger } from '@aztec/foundation/log';
|
|
8
13
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
9
14
|
import { Timer } from '@aztec/foundation/timer';
|
|
10
15
|
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
11
16
|
import { encodeSlashConsensusVotes } from '@aztec/slasher';
|
|
12
|
-
import {
|
|
13
|
-
import { ConsensusPayload, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
|
|
17
|
+
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
14
18
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
15
|
-
import pick from 'lodash.pick';
|
|
16
19
|
import { encodeFunctionData, toHex } from 'viem';
|
|
17
20
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
18
|
-
export var SignalType = /*#__PURE__*/ function(SignalType) {
|
|
19
|
-
SignalType[SignalType["GOVERNANCE"] = 0] = "GOVERNANCE";
|
|
20
|
-
SignalType[SignalType["SLASHING"] = 1] = "SLASHING";
|
|
21
|
-
return SignalType;
|
|
22
|
-
}({});
|
|
23
21
|
export const Actions = [
|
|
24
22
|
'invalidate-by-invalid-attestation',
|
|
25
23
|
'invalidate-by-insufficient-attestations',
|
|
@@ -40,10 +38,13 @@ export class SequencerPublisher {
|
|
|
40
38
|
epochCache;
|
|
41
39
|
governanceLog;
|
|
42
40
|
slashingLog;
|
|
43
|
-
|
|
41
|
+
lastActions;
|
|
42
|
+
isPayloadEmptyCache;
|
|
44
43
|
log;
|
|
45
44
|
ethereumSlotDuration;
|
|
46
|
-
|
|
45
|
+
blobClient;
|
|
46
|
+
/** Address to use for simulations in fisherman mode (actual proposer's address) */ proposerAddressForSimulation;
|
|
47
|
+
/** L1 fee analyzer for fisherman mode */ l1FeeAnalyzer;
|
|
47
48
|
// @note - with blobs, the below estimate seems too large.
|
|
48
49
|
// Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
|
|
49
50
|
// Total used for emptier block from above test: 429k (of which 84k is 1x blob)
|
|
@@ -63,17 +64,14 @@ export class SequencerPublisher {
|
|
|
63
64
|
this.interrupted = false;
|
|
64
65
|
this.governanceLog = createLogger('sequencer:publisher:governance');
|
|
65
66
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
[1]: 0n
|
|
69
|
-
};
|
|
70
|
-
this.log = createLogger('sequencer:publisher');
|
|
67
|
+
this.lastActions = {};
|
|
68
|
+
this.isPayloadEmptyCache = new Map();
|
|
71
69
|
this.requests = [];
|
|
70
|
+
this.log = deps.log ?? createLogger('sequencer:publisher');
|
|
72
71
|
this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
|
|
73
72
|
this.epochCache = deps.epochCache;
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
});
|
|
73
|
+
this.lastActions = deps.lastActions;
|
|
74
|
+
this.blobClient = deps.blobClient;
|
|
77
75
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
78
76
|
this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
|
|
79
77
|
this.l1TxUtils = deps.l1TxUtils;
|
|
@@ -86,6 +84,10 @@ export class SequencerPublisher {
|
|
|
86
84
|
this.slashingProposerContract = newSlashingProposer;
|
|
87
85
|
});
|
|
88
86
|
this.slashFactoryContract = deps.slashFactoryContract;
|
|
87
|
+
// Initialize L1 fee analyzer for fisherman mode
|
|
88
|
+
if (config.fishermanMode) {
|
|
89
|
+
this.l1FeeAnalyzer = new L1FeeAnalyzer(this.l1TxUtils.client, deps.dateProvider, createLogger('sequencer:publisher:fee-analyzer'));
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
92
|
getRollupContract() {
|
|
91
93
|
return this.rollupContract;
|
|
@@ -93,6 +95,17 @@ export class SequencerPublisher {
|
|
|
93
95
|
getSenderAddress() {
|
|
94
96
|
return this.l1TxUtils.getSenderAddress();
|
|
95
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Gets the L1 fee analyzer instance (only available in fisherman mode)
|
|
100
|
+
*/ getL1FeeAnalyzer() {
|
|
101
|
+
return this.l1FeeAnalyzer;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sets the proposer address to use for simulations in fisherman mode.
|
|
105
|
+
* @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
|
|
106
|
+
*/ setProposerAddressForSimulation(proposerAddress) {
|
|
107
|
+
this.proposerAddressForSimulation = proposerAddress;
|
|
108
|
+
}
|
|
96
109
|
addRequest(request) {
|
|
97
110
|
this.requests.push(request);
|
|
98
111
|
}
|
|
@@ -100,6 +113,55 @@ export class SequencerPublisher {
|
|
|
100
113
|
return this.epochCache.getEpochAndSlotNow().slot;
|
|
101
114
|
}
|
|
102
115
|
/**
|
|
116
|
+
* Clears all pending requests without sending them.
|
|
117
|
+
*/ clearPendingRequests() {
|
|
118
|
+
const count = this.requests.length;
|
|
119
|
+
this.requests = [];
|
|
120
|
+
if (count > 0) {
|
|
121
|
+
this.log.debug(`Cleared ${count} pending request(s)`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Analyzes L1 fees for the pending requests without sending them.
|
|
126
|
+
* This is used in fisherman mode to validate fee calculations.
|
|
127
|
+
* @param l2SlotNumber - The L2 slot number for this analysis
|
|
128
|
+
* @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
|
|
129
|
+
* @returns The analysis result (incomplete until block mines), or undefined if no requests
|
|
130
|
+
*/ async analyzeL1Fees(l2SlotNumber, onComplete) {
|
|
131
|
+
if (!this.l1FeeAnalyzer) {
|
|
132
|
+
this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const requestsToAnalyze = [
|
|
136
|
+
...this.requests
|
|
137
|
+
];
|
|
138
|
+
if (requestsToAnalyze.length === 0) {
|
|
139
|
+
this.log.debug('No requests to analyze for L1 fees');
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
// Extract blob config from requests (if any)
|
|
143
|
+
const blobConfigs = requestsToAnalyze.filter((request)=>request.blobConfig).map((request)=>request.blobConfig);
|
|
144
|
+
const blobConfig = blobConfigs[0];
|
|
145
|
+
// Get gas configs
|
|
146
|
+
const gasConfigs = requestsToAnalyze.filter((request)=>request.gasConfig).map((request)=>request.gasConfig);
|
|
147
|
+
const gasLimits = gasConfigs.map((g)=>g?.gasLimit).filter((g)=>g !== undefined);
|
|
148
|
+
const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g)=>sum + g, 0n) : 0n;
|
|
149
|
+
// Get the transaction requests
|
|
150
|
+
const l1Requests = requestsToAnalyze.map((r)=>r.request);
|
|
151
|
+
// Start the analysis
|
|
152
|
+
const analysisId = await this.l1FeeAnalyzer.startAnalysis(l2SlotNumber, gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS, l1Requests, blobConfig, onComplete);
|
|
153
|
+
this.log.info('Started L1 fee analysis', {
|
|
154
|
+
analysisId,
|
|
155
|
+
l2SlotNumber: l2SlotNumber.toString(),
|
|
156
|
+
requestCount: requestsToAnalyze.length,
|
|
157
|
+
hasBlobConfig: !!blobConfig,
|
|
158
|
+
gasLimit: gasLimit.toString(),
|
|
159
|
+
actions: requestsToAnalyze.map((r)=>r.action)
|
|
160
|
+
});
|
|
161
|
+
// Return the analysis result (will be incomplete until block mines)
|
|
162
|
+
return this.l1FeeAnalyzer.getAnalysis(analysisId);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
103
165
|
* Sends all requests that are still valid.
|
|
104
166
|
* @returns one of:
|
|
105
167
|
* - A receipt and stats if the tx succeeded
|
|
@@ -110,7 +172,7 @@ export class SequencerPublisher {
|
|
|
110
172
|
...this.requests
|
|
111
173
|
];
|
|
112
174
|
this.requests = [];
|
|
113
|
-
if (this.interrupted) {
|
|
175
|
+
if (this.interrupted || requestsToProcess.length === 0) {
|
|
114
176
|
return undefined;
|
|
115
177
|
}
|
|
116
178
|
const currentL2Slot = this.getCurrentL2Slot();
|
|
@@ -148,7 +210,7 @@ export class SequencerPublisher {
|
|
|
148
210
|
const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
149
211
|
const txTimeoutAts = gasConfigs.map((g)=>g?.txTimeoutAt).filter((g)=>g !== undefined);
|
|
150
212
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map((g)=>g.getTime()))) : undefined; // earliest
|
|
151
|
-
const
|
|
213
|
+
const txConfig = {
|
|
152
214
|
gasLimit,
|
|
153
215
|
txTimeoutAt
|
|
154
216
|
};
|
|
@@ -157,9 +219,10 @@ export class SequencerPublisher {
|
|
|
157
219
|
validRequests.sort((a, b)=>compareActions(a.action, b.action));
|
|
158
220
|
try {
|
|
159
221
|
this.log.debug('Forwarding transactions', {
|
|
160
|
-
validRequests: validRequests.map((request)=>request.action)
|
|
222
|
+
validRequests: validRequests.map((request)=>request.action),
|
|
223
|
+
txConfig
|
|
161
224
|
});
|
|
162
|
-
const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils,
|
|
225
|
+
const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, txConfig, blobConfig, this.rollupContract.address, this.log);
|
|
163
226
|
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
164
227
|
return {
|
|
165
228
|
result,
|
|
@@ -218,7 +281,9 @@ export class SequencerPublisher {
|
|
|
218
281
|
'InvalidProposer',
|
|
219
282
|
'InvalidArchive'
|
|
220
283
|
];
|
|
221
|
-
return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration,
|
|
284
|
+
return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
|
|
285
|
+
forcePendingCheckpointNumber: opts.forcePendingBlockNumber !== undefined ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber) : undefined
|
|
286
|
+
}).catch((err)=>{
|
|
222
287
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
223
288
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
224
289
|
error: err.message
|
|
@@ -241,16 +306,28 @@ export class SequencerPublisher {
|
|
|
241
306
|
};
|
|
242
307
|
const args = [
|
|
243
308
|
header.toViem(),
|
|
244
|
-
|
|
309
|
+
CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
|
|
245
310
|
[],
|
|
311
|
+
Signature.empty().toViemSignature(),
|
|
246
312
|
`0x${'0'.repeat(64)}`,
|
|
247
313
|
header.contentCommitment.blobsHash.toString(),
|
|
248
314
|
flags
|
|
249
315
|
];
|
|
250
316
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
317
|
+
const optsForcePendingCheckpointNumber = opts?.forcePendingBlockNumber !== undefined ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber) : undefined;
|
|
318
|
+
const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber);
|
|
319
|
+
let balance = 0n;
|
|
320
|
+
if (this.config.fishermanMode) {
|
|
321
|
+
// In fisherman mode, we can't know where the proposer is publishing from
|
|
322
|
+
// so we just add sufficient balance to the multicall3 address
|
|
323
|
+
balance = 10n * WEI_CONST * WEI_CONST; // 10 ETH
|
|
324
|
+
} else {
|
|
325
|
+
balance = await this.l1TxUtils.getSenderBalance();
|
|
326
|
+
}
|
|
327
|
+
stateOverrides.push({
|
|
328
|
+
address: MULTI_CALL_3_ADDRESS,
|
|
329
|
+
balance
|
|
330
|
+
});
|
|
254
331
|
await this.l1TxUtils.simulate({
|
|
255
332
|
to: this.rollupContract.address,
|
|
256
333
|
data: encodeFunctionData({
|
|
@@ -261,13 +338,7 @@ export class SequencerPublisher {
|
|
|
261
338
|
from: MULTI_CALL_3_ADDRESS
|
|
262
339
|
}, {
|
|
263
340
|
time: ts + 1n
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
address: MULTI_CALL_3_ADDRESS,
|
|
267
|
-
balance
|
|
268
|
-
},
|
|
269
|
-
...await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)
|
|
270
|
-
]);
|
|
341
|
+
}, stateOverrides);
|
|
271
342
|
this.log.debug(`Simulated validateHeader`);
|
|
272
343
|
}
|
|
273
344
|
/**
|
|
@@ -278,13 +349,13 @@ export class SequencerPublisher {
|
|
|
278
349
|
return undefined;
|
|
279
350
|
}
|
|
280
351
|
const { reason, block } = validationResult;
|
|
281
|
-
const blockNumber = block.
|
|
352
|
+
const blockNumber = block.blockNumber;
|
|
282
353
|
const logData = {
|
|
283
|
-
...block
|
|
354
|
+
...block,
|
|
284
355
|
reason
|
|
285
356
|
};
|
|
286
|
-
const currentBlockNumber = await this.rollupContract.
|
|
287
|
-
if (currentBlockNumber < validationResult.block.
|
|
357
|
+
const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
|
|
358
|
+
if (currentBlockNumber < validationResult.block.blockNumber) {
|
|
288
359
|
this.log.verbose(`Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`, {
|
|
289
360
|
currentBlockNumber,
|
|
290
361
|
...logData
|
|
@@ -292,7 +363,10 @@ export class SequencerPublisher {
|
|
|
292
363
|
return undefined;
|
|
293
364
|
}
|
|
294
365
|
const request = this.buildInvalidateBlockRequest(validationResult);
|
|
295
|
-
this.log.debug(`Simulating invalidate block ${blockNumber}`,
|
|
366
|
+
this.log.debug(`Simulating invalidate block ${blockNumber}`, {
|
|
367
|
+
...logData,
|
|
368
|
+
request
|
|
369
|
+
});
|
|
296
370
|
try {
|
|
297
371
|
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
|
|
298
372
|
this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, {
|
|
@@ -304,7 +378,7 @@ export class SequencerPublisher {
|
|
|
304
378
|
request,
|
|
305
379
|
gasUsed,
|
|
306
380
|
blockNumber,
|
|
307
|
-
forcePendingBlockNumber: blockNumber - 1,
|
|
381
|
+
forcePendingBlockNumber: BlockNumber(blockNumber - 1),
|
|
308
382
|
reason
|
|
309
383
|
};
|
|
310
384
|
} catch (err) {
|
|
@@ -317,7 +391,7 @@ export class SequencerPublisher {
|
|
|
317
391
|
request,
|
|
318
392
|
error: viemError.message
|
|
319
393
|
});
|
|
320
|
-
const latestPendingBlockNumber = await this.rollupContract.
|
|
394
|
+
const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
|
|
321
395
|
if (latestPendingBlockNumber < blockNumber) {
|
|
322
396
|
this.log.verbose(`Block number ${blockNumber} has already been invalidated`, {
|
|
323
397
|
...logData
|
|
@@ -343,66 +417,58 @@ export class SequencerPublisher {
|
|
|
343
417
|
}
|
|
344
418
|
const { block, committee, reason } = validationResult;
|
|
345
419
|
const logData = {
|
|
346
|
-
...block
|
|
420
|
+
...block,
|
|
347
421
|
reason
|
|
348
422
|
};
|
|
349
|
-
this.log.debug(`Simulating invalidate block ${block.
|
|
423
|
+
this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
|
|
424
|
+
const attestationsAndSigners = new CommitteeAttestationsAndSigners(validationResult.attestations).getPackedAttestations();
|
|
350
425
|
if (reason === 'invalid-attestation') {
|
|
351
|
-
return this.rollupContract.buildInvalidateBadAttestationRequest(
|
|
426
|
+
return this.rollupContract.buildInvalidateBadAttestationRequest(CheckpointNumber.fromBlockNumber(block.blockNumber), attestationsAndSigners, committee, validationResult.invalidIndex);
|
|
352
427
|
} else if (reason === 'insufficient-attestations') {
|
|
353
|
-
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
|
|
428
|
+
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(CheckpointNumber.fromBlockNumber(block.blockNumber), attestationsAndSigners, committee);
|
|
354
429
|
} else {
|
|
355
430
|
const _ = reason;
|
|
356
431
|
throw new Error(`Unknown reason for invalidation`);
|
|
357
432
|
}
|
|
358
433
|
}
|
|
359
|
-
/**
|
|
360
|
-
* @notice Will simulate `propose` to make sure that the block is valid for submission
|
|
361
|
-
*
|
|
362
|
-
* @dev Throws if unable to propose
|
|
363
|
-
*
|
|
364
|
-
* @param block - The block to propose
|
|
365
|
-
* @param attestationData - The block's attestation data
|
|
366
|
-
*
|
|
367
|
-
*/ async validateBlockForSubmission(block, attestationData = {
|
|
368
|
-
digest: Buffer.alloc(32),
|
|
369
|
-
attestations: []
|
|
370
|
-
}, options) {
|
|
434
|
+
/** Simulates `propose` to make sure that the checkpoint is valid for submission */ async validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, options) {
|
|
371
435
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
436
|
+
// TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
|
|
372
437
|
// If we have no attestations, we still need to provide the empty attestations
|
|
373
438
|
// so that the committee is recalculated correctly
|
|
374
|
-
const ignoreSignatures =
|
|
375
|
-
if (ignoreSignatures) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
const
|
|
439
|
+
// const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
|
|
440
|
+
// if (ignoreSignatures) {
|
|
441
|
+
// const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
|
|
442
|
+
// if (!committee) {
|
|
443
|
+
// this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
444
|
+
// throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
|
|
445
|
+
// }
|
|
446
|
+
// attestationsAndSigners.attestations = committee.map(committeeMember =>
|
|
447
|
+
// CommitteeAttestation.fromAddress(committeeMember),
|
|
448
|
+
// );
|
|
449
|
+
// }
|
|
450
|
+
const blobFields = checkpoint.toBlobFields();
|
|
451
|
+
const blobs = getBlobsPerL1Block(blobFields);
|
|
452
|
+
const blobInput = getPrefixedEthBlobCommitments(blobs);
|
|
387
453
|
const args = [
|
|
388
454
|
{
|
|
389
|
-
header:
|
|
390
|
-
archive: toHex(
|
|
391
|
-
stateReference: block.header.state.toViem(),
|
|
392
|
-
txHashes: block.body.txEffects.map((txEffect)=>txEffect.txHash.toString()),
|
|
455
|
+
header: checkpoint.header.toViem(),
|
|
456
|
+
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
393
457
|
oracleInput: {
|
|
394
458
|
feeAssetPriceModifier: 0n
|
|
395
459
|
}
|
|
396
460
|
},
|
|
397
|
-
|
|
398
|
-
|
|
461
|
+
attestationsAndSigners.getPackedAttestations(),
|
|
462
|
+
attestationsAndSigners.getSigners().map((signer)=>signer.toString()),
|
|
463
|
+
attestationsAndSignersSignature.toViemSignature(),
|
|
399
464
|
blobInput
|
|
400
465
|
];
|
|
401
466
|
await this.simulateProposeTx(args, ts, options);
|
|
402
467
|
return ts;
|
|
403
468
|
}
|
|
404
469
|
async enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer) {
|
|
405
|
-
if (this.
|
|
470
|
+
if (this.lastActions[signalType] && this.lastActions[signalType] === slotNumber) {
|
|
471
|
+
this.log.debug(`Skipping duplicate vote cast signal ${signalType} for slot ${slotNumber}`);
|
|
406
472
|
return false;
|
|
407
473
|
}
|
|
408
474
|
if (payload.equals(EthAddress.ZERO)) {
|
|
@@ -414,12 +480,19 @@ export class SequencerPublisher {
|
|
|
414
480
|
}
|
|
415
481
|
const round = await base.computeRound(slotNumber);
|
|
416
482
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
483
|
+
if (roundInfo.quorumReached) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
417
486
|
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
418
487
|
return false;
|
|
419
488
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
489
|
+
if (await this.isPayloadEmpty(payload)) {
|
|
490
|
+
this.log.warn(`Skipping vote cast for payload with empty code`);
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
const cachedLastVote = this.lastActions[signalType];
|
|
494
|
+
this.lastActions[signalType] = slotNumber;
|
|
495
|
+
const action = signalType;
|
|
423
496
|
const request = await base.createSignalRequestWithSignature(payload.toString(), slotNumber, this.config.l1ChainId, signerAddress.toString(), signer);
|
|
424
497
|
this.log.debug(`Created ${action} request with signature`, {
|
|
425
498
|
request,
|
|
@@ -455,24 +528,34 @@ export class SequencerPublisher {
|
|
|
455
528
|
payload: payload.toString()
|
|
456
529
|
};
|
|
457
530
|
if (!success) {
|
|
458
|
-
this.log.error(`Signaling in
|
|
459
|
-
this.
|
|
531
|
+
this.log.error(`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`, logData);
|
|
532
|
+
this.lastActions[signalType] = cachedLastVote;
|
|
460
533
|
return false;
|
|
461
534
|
} else {
|
|
462
|
-
this.log.info(`Signaling in
|
|
535
|
+
this.log.info(`Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`, logData);
|
|
463
536
|
return true;
|
|
464
537
|
}
|
|
465
538
|
}
|
|
466
539
|
});
|
|
467
540
|
return true;
|
|
468
541
|
}
|
|
542
|
+
async isPayloadEmpty(payload) {
|
|
543
|
+
const key = payload.toString();
|
|
544
|
+
const cached = this.isPayloadEmptyCache.get(key);
|
|
545
|
+
if (cached) {
|
|
546
|
+
return cached;
|
|
547
|
+
}
|
|
548
|
+
const isEmpty = !await this.l1TxUtils.getCode(payload);
|
|
549
|
+
this.isPayloadEmptyCache.set(key, isEmpty);
|
|
550
|
+
return isEmpty;
|
|
551
|
+
}
|
|
469
552
|
/**
|
|
470
553
|
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
|
|
471
554
|
* @param slotNumber - The slot number to cast a signal for.
|
|
472
555
|
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
473
556
|
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
474
557
|
*/ enqueueGovernanceCastSignal(governancePayload, slotNumber, timestamp, signerAddress, signer) {
|
|
475
|
-
return this.enqueueCastSignalHelper(slotNumber, timestamp,
|
|
558
|
+
return this.enqueueCastSignalHelper(slotNumber, timestamp, 'governance-signal', governancePayload, this.govProposerContract, signerAddress, signer);
|
|
476
559
|
}
|
|
477
560
|
/** Enqueues all slashing actions as returned by the slasher client. */ async enqueueSlashingActions(actions, slotNumber, timestamp, signerAddress, signer) {
|
|
478
561
|
if (actions.length === 0) {
|
|
@@ -490,7 +573,7 @@ export class SequencerPublisher {
|
|
|
490
573
|
this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
|
|
491
574
|
signerAddress
|
|
492
575
|
});
|
|
493
|
-
await this.enqueueCastSignalHelper(slotNumber, timestamp,
|
|
576
|
+
await this.enqueueCastSignalHelper(slotNumber, timestamp, 'empire-slashing-signal', action.payload, this.slashingProposerContract, signerAddress, signer);
|
|
494
577
|
break;
|
|
495
578
|
}
|
|
496
579
|
case 'create-empire-payload':
|
|
@@ -561,24 +644,16 @@ export class SequencerPublisher {
|
|
|
561
644
|
}
|
|
562
645
|
return true;
|
|
563
646
|
}
|
|
564
|
-
/**
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
* @returns True if the tx has been enqueued, throws otherwise. See #9315
|
|
569
|
-
*/ async enqueueProposeL2Block(block, attestations, txHashes, opts = {}) {
|
|
570
|
-
const proposedBlockHeader = block.header.toPropose();
|
|
571
|
-
const consensusPayload = ConsensusPayload.fromBlock(block);
|
|
572
|
-
const digest = getHashedSignaturePayload(consensusPayload, SignatureDomainSeparator.blockAttestation);
|
|
573
|
-
const blobs = await Blob.getBlobsPerBlock(block.body.toBlobFields());
|
|
647
|
+
/** Simulates and enqueues a proposal for a checkpoint on L1 */ async enqueueProposeCheckpoint(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts = {}) {
|
|
648
|
+
const checkpointHeader = checkpoint.header;
|
|
649
|
+
const blobFields = checkpoint.toBlobFields();
|
|
650
|
+
const blobs = getBlobsPerL1Block(blobFields);
|
|
574
651
|
const proposeTxArgs = {
|
|
575
|
-
header:
|
|
576
|
-
archive:
|
|
577
|
-
stateReference: block.header.state,
|
|
578
|
-
body: block.body.toBuffer(),
|
|
652
|
+
header: checkpointHeader,
|
|
653
|
+
archive: checkpoint.archive.root.toBuffer(),
|
|
579
654
|
blobs,
|
|
580
|
-
|
|
581
|
-
|
|
655
|
+
attestationsAndSigners,
|
|
656
|
+
attestationsAndSignersSignature
|
|
582
657
|
};
|
|
583
658
|
let ts;
|
|
584
659
|
try {
|
|
@@ -586,26 +661,21 @@ export class SequencerPublisher {
|
|
|
586
661
|
// This means that we can avoid the simulation issues in later checks.
|
|
587
662
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
588
663
|
// make time consistency checks break.
|
|
589
|
-
const attestationData = {
|
|
590
|
-
digest: digest.toBuffer(),
|
|
591
|
-
attestations: attestations ?? []
|
|
592
|
-
};
|
|
593
664
|
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
594
|
-
ts = await this.
|
|
665
|
+
ts = await this.validateCheckpointForSubmission(checkpoint, attestationsAndSigners, attestationsAndSignersSignature, opts);
|
|
595
666
|
} catch (err) {
|
|
596
|
-
this.log.error(`
|
|
597
|
-
...
|
|
598
|
-
slotNumber:
|
|
667
|
+
this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
668
|
+
...checkpoint.getStats(),
|
|
669
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
599
670
|
forcePendingBlockNumber: opts.forcePendingBlockNumber
|
|
600
671
|
});
|
|
601
672
|
throw err;
|
|
602
673
|
}
|
|
603
|
-
this.log.verbose(`Enqueuing
|
|
604
|
-
...
|
|
674
|
+
this.log.verbose(`Enqueuing checkpoint propose transaction`, {
|
|
675
|
+
...checkpoint.toCheckpointInfo(),
|
|
605
676
|
...opts
|
|
606
677
|
});
|
|
607
|
-
await this.addProposeTx(
|
|
608
|
-
return true;
|
|
678
|
+
await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
|
|
609
679
|
}
|
|
610
680
|
enqueueInvalidateBlock(request, opts = {}) {
|
|
611
681
|
if (!request) {
|
|
@@ -613,8 +683,10 @@ export class SequencerPublisher {
|
|
|
613
683
|
}
|
|
614
684
|
// We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
615
685
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(request.gasUsed) * 64 / 63)));
|
|
686
|
+
const { gasUsed, blockNumber } = request;
|
|
616
687
|
const logData = {
|
|
617
|
-
|
|
688
|
+
gasUsed,
|
|
689
|
+
blockNumber,
|
|
618
690
|
gasLimit,
|
|
619
691
|
opts
|
|
620
692
|
};
|
|
@@ -626,9 +698,9 @@ export class SequencerPublisher {
|
|
|
626
698
|
gasLimit,
|
|
627
699
|
txTimeoutAt: opts.txTimeoutAt
|
|
628
700
|
},
|
|
629
|
-
lastValidL2Slot: this.getCurrentL2Slot() +
|
|
701
|
+
lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
|
|
630
702
|
checkSuccess: (_req, result)=>{
|
|
631
|
-
const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
703
|
+
const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
|
|
632
704
|
if (!success) {
|
|
633
705
|
this.log.warn(`Invalidate block ${request.blockNumber} failed`, {
|
|
634
706
|
...result,
|
|
@@ -650,8 +722,14 @@ export class SequencerPublisher {
|
|
|
650
722
|
timestamp,
|
|
651
723
|
gasLimit: undefined
|
|
652
724
|
};
|
|
725
|
+
if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
|
|
726
|
+
this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
const cachedLastActionSlot = this.lastActions[action];
|
|
730
|
+
this.lastActions[action] = slotNumber;
|
|
731
|
+
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
653
732
|
let gasUsed;
|
|
654
|
-
this.log.debug(`Simulating ${action}`, logData);
|
|
655
733
|
try {
|
|
656
734
|
({ gasUsed } = await this.l1TxUtils.simulate(request, {
|
|
657
735
|
time: timestamp
|
|
@@ -684,6 +762,7 @@ export class SequencerPublisher {
|
|
|
684
762
|
...result,
|
|
685
763
|
...logData
|
|
686
764
|
});
|
|
765
|
+
this.lastActions[action] = cachedLastActionSlot;
|
|
687
766
|
} else {
|
|
688
767
|
this.log.info(`Action ${action} at ${slotNumber} succeeded`, {
|
|
689
768
|
...result,
|
|
@@ -710,45 +789,52 @@ export class SequencerPublisher {
|
|
|
710
789
|
}
|
|
711
790
|
async prepareProposeTx(encodedData, timestamp, options) {
|
|
712
791
|
const kzg = Blob.getViemKzgInstance();
|
|
713
|
-
const blobInput =
|
|
792
|
+
const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
|
|
714
793
|
this.log.debug('Validating blob input', {
|
|
715
794
|
blobInput
|
|
716
795
|
});
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
796
|
+
// Get blob evaluation gas
|
|
797
|
+
let blobEvaluationGas;
|
|
798
|
+
if (this.config.fishermanMode) {
|
|
799
|
+
// In fisherman mode, we can't estimate blob gas because estimateGas doesn't support state overrides
|
|
800
|
+
// Use a fixed estimate.
|
|
801
|
+
blobEvaluationGas = BigInt(encodedData.blobs.length) * 21_000n;
|
|
802
|
+
this.log.debug(`Using fixed blob evaluation gas estimate in fisherman mode: ${blobEvaluationGas}`);
|
|
803
|
+
} else {
|
|
804
|
+
// Normal mode - use estimateGas with blob inputs
|
|
805
|
+
blobEvaluationGas = await this.l1TxUtils.estimateGas(this.getSenderAddress().toString(), {
|
|
806
|
+
to: this.rollupContract.address,
|
|
807
|
+
data: encodeFunctionData({
|
|
808
|
+
abi: RollupAbi,
|
|
809
|
+
functionName: 'validateBlobs',
|
|
810
|
+
args: [
|
|
811
|
+
blobInput
|
|
812
|
+
]
|
|
813
|
+
})
|
|
814
|
+
}, {}, {
|
|
815
|
+
blobs: encodedData.blobs.map((b)=>b.data),
|
|
816
|
+
kzg
|
|
817
|
+
}).catch((err)=>{
|
|
818
|
+
const { message, metaMessages } = formatViemError(err);
|
|
819
|
+
this.log.error(`Failed to validate blobs`, message, {
|
|
820
|
+
metaMessages
|
|
821
|
+
});
|
|
822
|
+
throw new Error('Failed to validate blobs');
|
|
733
823
|
});
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const attestations = encodedData.attestations ? encodedData.attestations.map((attest)=>attest.toViem()) : [];
|
|
737
|
-
const txHashes = encodedData.txHashes ? encodedData.txHashes.map((txHash)=>txHash.toString()) : [];
|
|
738
|
-
const signers = encodedData.attestations?.filter((attest)=>!attest.signature.isEmpty()).map((attest)=>attest.address.toString());
|
|
824
|
+
}
|
|
825
|
+
const signers = encodedData.attestationsAndSigners.getSigners().map((signer)=>signer.toString());
|
|
739
826
|
const args = [
|
|
740
827
|
{
|
|
741
828
|
header: encodedData.header.toViem(),
|
|
742
829
|
archive: toHex(encodedData.archive),
|
|
743
|
-
stateReference: encodedData.stateReference.toViem(),
|
|
744
830
|
oracleInput: {
|
|
745
831
|
// We are currently not modifying these. See #9963
|
|
746
832
|
feeAssetPriceModifier: 0n
|
|
747
|
-
}
|
|
748
|
-
txHashes
|
|
833
|
+
}
|
|
749
834
|
},
|
|
750
|
-
|
|
751
|
-
signers
|
|
835
|
+
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
836
|
+
signers,
|
|
837
|
+
encodedData.attestationsAndSignersSignature.toViemSignature(),
|
|
752
838
|
blobInput
|
|
753
839
|
];
|
|
754
840
|
const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
|
|
@@ -770,18 +856,10 @@ export class SequencerPublisher {
|
|
|
770
856
|
functionName: 'propose',
|
|
771
857
|
args
|
|
772
858
|
});
|
|
773
|
-
// override the pending
|
|
774
|
-
const
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
data: rollupData,
|
|
778
|
-
gas: SequencerPublisher.PROPOSE_GAS_GUESS
|
|
779
|
-
}, {
|
|
780
|
-
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
781
|
-
time: timestamp + 1n,
|
|
782
|
-
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
783
|
-
gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n
|
|
784
|
-
}, [
|
|
859
|
+
// override the pending checkpoint number if requested
|
|
860
|
+
const optsForcePendingCheckpointNumber = options.forcePendingBlockNumber !== undefined ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber) : undefined;
|
|
861
|
+
const forcePendingCheckpointNumberStateDiff = (optsForcePendingCheckpointNumber !== undefined ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
862
|
+
const stateOverrides = [
|
|
785
863
|
{
|
|
786
864
|
address: this.rollupContract.address,
|
|
787
865
|
// @note we override checkBlob to false since blobs are not part simulate()
|
|
@@ -790,14 +868,44 @@ export class SequencerPublisher {
|
|
|
790
868
|
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
791
869
|
value: toPaddedHex(0n, true)
|
|
792
870
|
},
|
|
793
|
-
...
|
|
871
|
+
...forcePendingCheckpointNumberStateDiff
|
|
794
872
|
]
|
|
795
873
|
}
|
|
796
|
-
]
|
|
874
|
+
];
|
|
875
|
+
// In fisherman mode, simulate as the proposer but with sufficient balance
|
|
876
|
+
if (this.proposerAddressForSimulation) {
|
|
877
|
+
stateOverrides.push({
|
|
878
|
+
address: this.proposerAddressForSimulation.toString(),
|
|
879
|
+
balance: 10n * WEI_CONST * WEI_CONST
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
const simulationResult = await this.l1TxUtils.simulate({
|
|
883
|
+
to: this.rollupContract.address,
|
|
884
|
+
data: rollupData,
|
|
885
|
+
gas: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
886
|
+
...this.proposerAddressForSimulation && {
|
|
887
|
+
from: this.proposerAddressForSimulation.toString()
|
|
888
|
+
}
|
|
889
|
+
}, {
|
|
890
|
+
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
891
|
+
time: timestamp + 1n,
|
|
892
|
+
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
893
|
+
gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n
|
|
894
|
+
}, stateOverrides, RollupAbi, {
|
|
797
895
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
798
896
|
fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS
|
|
799
897
|
}).catch((err)=>{
|
|
800
|
-
|
|
898
|
+
// In fisherman mode, we expect ValidatorSelection__MissingProposerSignature since fisherman doesn't have proposer signature
|
|
899
|
+
const viemError = formatViemError(err);
|
|
900
|
+
if (this.config.fishermanMode && viemError.message?.includes('ValidatorSelection__MissingProposerSignature')) {
|
|
901
|
+
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
902
|
+
// Return a minimal simulation result with the fallback gas estimate
|
|
903
|
+
return {
|
|
904
|
+
gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
|
|
905
|
+
logs: []
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
this.log.error(`Failed to simulate propose tx`, viemError);
|
|
801
909
|
throw err;
|
|
802
910
|
});
|
|
803
911
|
return {
|
|
@@ -805,24 +913,25 @@ export class SequencerPublisher {
|
|
|
805
913
|
simulationResult
|
|
806
914
|
};
|
|
807
915
|
}
|
|
808
|
-
async addProposeTx(
|
|
916
|
+
async addProposeTx(checkpoint, encodedData, opts = {}, timestamp) {
|
|
917
|
+
const slot = checkpoint.header.slotNumber;
|
|
809
918
|
const timer = new Timer();
|
|
810
919
|
const kzg = Blob.getViemKzgInstance();
|
|
811
920
|
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
|
|
812
921
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
813
922
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
814
|
-
// Send the blobs to the blob
|
|
815
|
-
// tx fails but it does get mined. We make sure that the blobs are sent to the blob
|
|
816
|
-
void this.
|
|
817
|
-
|
|
818
|
-
|
|
923
|
+
// Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
924
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
|
|
925
|
+
void Promise.resolve().then(()=>this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch((_err)=>{
|
|
926
|
+
this.log.error('Failed to send blobs to blob client');
|
|
927
|
+
}));
|
|
819
928
|
return this.addRequest({
|
|
820
929
|
action: 'propose',
|
|
821
930
|
request: {
|
|
822
931
|
to: this.rollupContract.address,
|
|
823
932
|
data: rollupData
|
|
824
933
|
},
|
|
825
|
-
lastValidL2Slot:
|
|
934
|
+
lastValidL2Slot: checkpoint.header.slotNumber,
|
|
826
935
|
gasConfig: {
|
|
827
936
|
...opts,
|
|
828
937
|
gasLimit
|
|
@@ -836,36 +945,37 @@ export class SequencerPublisher {
|
|
|
836
945
|
return false;
|
|
837
946
|
}
|
|
838
947
|
const { receipt, stats, errorMsg } = result;
|
|
839
|
-
const success = receipt && receipt.status === 'success' && tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, '
|
|
948
|
+
const success = receipt && receipt.status === 'success' && tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
|
|
840
949
|
if (success) {
|
|
841
950
|
const endBlock = receipt.blockNumber;
|
|
842
951
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
952
|
+
const { calldataGas, calldataSize, sender } = stats;
|
|
843
953
|
const publishStats = {
|
|
844
954
|
gasPrice: receipt.effectiveGasPrice,
|
|
845
955
|
gasUsed: receipt.gasUsed,
|
|
846
956
|
blobGasUsed: receipt.blobGasUsed ?? 0n,
|
|
847
957
|
blobDataGas: receipt.blobGasPrice ?? 0n,
|
|
848
958
|
transactionHash: receipt.transactionHash,
|
|
849
|
-
|
|
850
|
-
|
|
959
|
+
calldataGas,
|
|
960
|
+
calldataSize,
|
|
961
|
+
sender,
|
|
962
|
+
...checkpoint.getStats(),
|
|
851
963
|
eventName: 'rollup-published-to-l1',
|
|
852
964
|
blobCount: encodedData.blobs.length,
|
|
853
965
|
inclusionBlocks
|
|
854
966
|
};
|
|
855
|
-
this.log.info(`Published
|
|
967
|
+
this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
|
|
856
968
|
...stats,
|
|
857
|
-
...
|
|
858
|
-
...receipt
|
|
969
|
+
...checkpoint.getStats(),
|
|
970
|
+
...pick(receipt, 'transactionHash', 'blockHash')
|
|
859
971
|
});
|
|
860
972
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
861
973
|
return true;
|
|
862
974
|
} else {
|
|
863
975
|
this.metrics.recordFailedTx('process');
|
|
864
|
-
this.log.error(`
|
|
865
|
-
...
|
|
866
|
-
receipt
|
|
867
|
-
txHash: receipt.transactionHash,
|
|
868
|
-
slotNumber: block.header.globalVariables.slotNumber.toBigInt()
|
|
976
|
+
this.log.error(`Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`, undefined, {
|
|
977
|
+
...checkpoint.getStats(),
|
|
978
|
+
...receipt
|
|
869
979
|
});
|
|
870
980
|
return false;
|
|
871
981
|
}
|