@aztec/sequencer-client 1.2.1 → 2.0.0-nightly.20250813
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 +0 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +5 -7
- package/dest/config.d.ts +1 -0
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +27 -0
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +48 -33
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +272 -114
- package/dest/sequencer/block_builder.d.ts +2 -7
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +3 -7
- package/dest/sequencer/sequencer.d.ts +27 -15
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +141 -85
- package/dest/sequencer/timetable.d.ts +15 -5
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +25 -12
- package/dest/sequencer/utils.d.ts +1 -11
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +0 -23
- package/package.json +26 -26
- package/src/client/sequencer-client.ts +4 -8
- package/src/config.ts +33 -0
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +318 -131
- package/src/sequencer/block_builder.ts +15 -8
- package/src/sequencer/sequencer.ts +217 -87
- package/src/sequencer/timetable.ts +43 -7
- package/src/sequencer/utils.ts +6 -37
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { Blob } from '@aztec/blob-lib';
|
|
2
2
|
import { createBlobSinkClient } from '@aztec/blob-sink/client';
|
|
3
|
-
import { FormattedViemError, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract, formatViemError } from '@aztec/ethereum';
|
|
3
|
+
import { FormattedViemError, MULTI_CALL_3_ADDRESS, Multicall3, RollupContract, formatViemError, tryExtractEvent } from '@aztec/ethereum';
|
|
4
4
|
import { sumBigint } from '@aztec/foundation/bigint';
|
|
5
5
|
import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
|
|
6
6
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
8
8
|
import { Timer } from '@aztec/foundation/timer';
|
|
9
|
-
import { RollupAbi } from '@aztec/l1-artifacts';
|
|
9
|
+
import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
|
|
10
10
|
import { CommitteeAttestation } from '@aztec/stdlib/block';
|
|
11
11
|
import { ConsensusPayload, SignatureDomainSeparator, getHashedSignaturePayload } from '@aztec/stdlib/p2p';
|
|
12
12
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
13
13
|
import pick from 'lodash.pick';
|
|
14
|
-
import { encodeFunctionData,
|
|
14
|
+
import { encodeFunctionData, toHex } from 'viem';
|
|
15
15
|
import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
|
|
16
|
-
export var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
16
|
+
export var SignalType = /*#__PURE__*/ function(SignalType) {
|
|
17
|
+
SignalType[SignalType["GOVERNANCE"] = 0] = "GOVERNANCE";
|
|
18
|
+
SignalType[SignalType["SLASHING"] = 1] = "SLASHING";
|
|
19
|
+
return SignalType;
|
|
20
20
|
}({});
|
|
21
21
|
const Actions = [
|
|
22
22
|
'propose',
|
|
23
|
-
'governance-
|
|
24
|
-
'slashing-
|
|
23
|
+
'governance-signal',
|
|
24
|
+
'slashing-signal',
|
|
25
|
+
'invalidate-by-invalid-attestation',
|
|
26
|
+
'invalidate-by-insufficient-attestations'
|
|
25
27
|
];
|
|
26
|
-
// Sorting for actions such that proposals
|
|
28
|
+
// Sorting for actions such that invalidations go first, then proposals, and last votes
|
|
27
29
|
const compareActions = (a, b)=>Actions.indexOf(b) - Actions.indexOf(a);
|
|
28
30
|
export class SequencerPublisher {
|
|
29
31
|
config;
|
|
@@ -31,12 +33,10 @@ export class SequencerPublisher {
|
|
|
31
33
|
metrics;
|
|
32
34
|
epochCache;
|
|
33
35
|
governanceLog;
|
|
34
|
-
governanceProposerAddress;
|
|
35
36
|
governancePayload;
|
|
36
37
|
slashingLog;
|
|
37
|
-
slashingProposerAddress;
|
|
38
38
|
getSlashPayload;
|
|
39
|
-
|
|
39
|
+
myLastSignals;
|
|
40
40
|
log;
|
|
41
41
|
ethereumSlotDuration;
|
|
42
42
|
blobSinkClient;
|
|
@@ -60,7 +60,7 @@ export class SequencerPublisher {
|
|
|
60
60
|
this.governancePayload = EthAddress.ZERO;
|
|
61
61
|
this.slashingLog = createLogger('sequencer:publisher:slashing');
|
|
62
62
|
this.getSlashPayload = undefined;
|
|
63
|
-
this.
|
|
63
|
+
this.myLastSignals = {
|
|
64
64
|
[0]: 0n,
|
|
65
65
|
[1]: 0n
|
|
66
66
|
};
|
|
@@ -77,6 +77,11 @@ export class SequencerPublisher {
|
|
|
77
77
|
this.rollupContract = deps.rollupContract;
|
|
78
78
|
this.govProposerContract = deps.governanceProposerContract;
|
|
79
79
|
this.slashingProposerContract = deps.slashingProposerContract;
|
|
80
|
+
this.rollupContract.listenToSlasherChanged(async ()=>{
|
|
81
|
+
this.log.info('Slashing proposer changed');
|
|
82
|
+
const newSlashingProposer = await this.rollupContract.getSlashingProposer();
|
|
83
|
+
this.slashingProposerContract = newSlashingProposer;
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
getRollupContract() {
|
|
82
87
|
return this.rollupContract;
|
|
@@ -160,11 +165,13 @@ export class SequencerPublisher {
|
|
|
160
165
|
validRequests: validRequests.map((request)=>request.action)
|
|
161
166
|
});
|
|
162
167
|
const result = await Multicall3.forward(validRequests.map((request)=>request.request), this.l1TxUtils, gasConfig, blobConfig, this.rollupContract.address, this.log);
|
|
163
|
-
this.callbackBundledTransactions(validRequests, result);
|
|
168
|
+
const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
|
|
164
169
|
return {
|
|
165
170
|
result,
|
|
166
171
|
expiredActions,
|
|
167
|
-
validActions
|
|
172
|
+
sentActions: validActions,
|
|
173
|
+
successfulActions,
|
|
174
|
+
failedActions
|
|
168
175
|
};
|
|
169
176
|
} catch (err) {
|
|
170
177
|
const viemError = formatViemError(err);
|
|
@@ -179,31 +186,44 @@ export class SequencerPublisher {
|
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
callbackBundledTransactions(requests, result) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
const actionsListStr = requests.map((r)=>r.action).join(', ');
|
|
190
|
+
if (result instanceof FormattedViemError) {
|
|
191
|
+
this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
|
|
192
|
+
return {
|
|
193
|
+
failedActions: requests.map((r)=>r.action)
|
|
194
|
+
};
|
|
195
|
+
} else {
|
|
196
|
+
this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
|
|
197
|
+
result,
|
|
198
|
+
requests
|
|
199
|
+
});
|
|
200
|
+
const successfulActions = [];
|
|
201
|
+
const failedActions = [];
|
|
202
|
+
for (const request of requests){
|
|
203
|
+
if (request.checkSuccess(request.request, result)) {
|
|
204
|
+
successfulActions.push(request.action);
|
|
205
|
+
} else {
|
|
206
|
+
failedActions.push(request.action);
|
|
207
|
+
}
|
|
189
208
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
return {
|
|
210
|
+
successfulActions,
|
|
211
|
+
failedActions
|
|
212
|
+
};
|
|
193
213
|
}
|
|
194
214
|
}
|
|
195
215
|
/**
|
|
196
216
|
* @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
|
|
197
217
|
* @param tipArchive - The archive to check
|
|
198
218
|
* @returns The slot and block number if it is possible to propose, undefined otherwise
|
|
199
|
-
*/ canProposeAtNextEthBlock(tipArchive, msgSender) {
|
|
219
|
+
*/ canProposeAtNextEthBlock(tipArchive, msgSender, opts = {}) {
|
|
200
220
|
// TODO: #14291 - should loop through multiple keys to check if any of them can propose
|
|
201
221
|
const ignoredErrors = [
|
|
202
222
|
'SlotAlreadyInChain',
|
|
203
223
|
'InvalidProposer',
|
|
204
224
|
'InvalidArchive'
|
|
205
225
|
];
|
|
206
|
-
return this.rollupContract.canProposeAtNextEthBlock(tipArchive, msgSender.toString(), this.ethereumSlotDuration).catch((err)=>{
|
|
226
|
+
return this.rollupContract.canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, opts).catch((err)=>{
|
|
207
227
|
if (err instanceof FormattedViemError && ignoredErrors.find((e)=>err.message.includes(e))) {
|
|
208
228
|
this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find((e)=>err.message.includes(e))}`, {
|
|
209
229
|
error: err.message
|
|
@@ -219,7 +239,7 @@ export class SequencerPublisher {
|
|
|
219
239
|
* @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
|
|
220
240
|
* It will throw if the block header is invalid.
|
|
221
241
|
* @param header - The block header to validate
|
|
222
|
-
*/ async validateBlockHeader(header) {
|
|
242
|
+
*/ async validateBlockHeader(header, opts) {
|
|
223
243
|
const flags = {
|
|
224
244
|
ignoreDA: true,
|
|
225
245
|
ignoreSignatures: true
|
|
@@ -227,6 +247,7 @@ export class SequencerPublisher {
|
|
|
227
247
|
const args = [
|
|
228
248
|
header.toViem(),
|
|
229
249
|
RollupContract.packAttestations([]),
|
|
250
|
+
[],
|
|
230
251
|
`0x${'0'.repeat(64)}`,
|
|
231
252
|
header.contentCommitment.blobsHash.toString(),
|
|
232
253
|
flags
|
|
@@ -238,7 +259,7 @@ export class SequencerPublisher {
|
|
|
238
259
|
to: this.rollupContract.address,
|
|
239
260
|
data: encodeFunctionData({
|
|
240
261
|
abi: RollupAbi,
|
|
241
|
-
functionName: '
|
|
262
|
+
functionName: 'validateHeaderWithAttestations',
|
|
242
263
|
args
|
|
243
264
|
}),
|
|
244
265
|
from: MULTI_CALL_3_ADDRESS
|
|
@@ -248,10 +269,97 @@ export class SequencerPublisher {
|
|
|
248
269
|
{
|
|
249
270
|
address: MULTI_CALL_3_ADDRESS,
|
|
250
271
|
balance
|
|
251
|
-
}
|
|
272
|
+
},
|
|
273
|
+
...await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)
|
|
252
274
|
]);
|
|
253
275
|
}
|
|
254
276
|
/**
|
|
277
|
+
* Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
|
|
278
|
+
* @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
|
|
279
|
+
*/ async simulateInvalidateBlock(validationResult) {
|
|
280
|
+
if (validationResult.valid) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
const { reason, block } = validationResult;
|
|
284
|
+
const blockNumber = block.block.number;
|
|
285
|
+
const logData = {
|
|
286
|
+
...block.block.toBlockInfo(),
|
|
287
|
+
reason
|
|
288
|
+
};
|
|
289
|
+
const currentBlockNumber = await this.rollupContract.getBlockNumber();
|
|
290
|
+
if (currentBlockNumber < validationResult.block.block.number) {
|
|
291
|
+
this.log.verbose(`Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`, {
|
|
292
|
+
currentBlockNumber,
|
|
293
|
+
...logData
|
|
294
|
+
});
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const request = this.buildInvalidateBlockRequest(validationResult);
|
|
298
|
+
this.log.debug(`Simulating invalidate block ${blockNumber}`, logData);
|
|
299
|
+
try {
|
|
300
|
+
const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
|
|
301
|
+
this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, {
|
|
302
|
+
...logData,
|
|
303
|
+
request,
|
|
304
|
+
gasUsed
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
request,
|
|
308
|
+
gasUsed,
|
|
309
|
+
blockNumber,
|
|
310
|
+
forcePendingBlockNumber: blockNumber - 1,
|
|
311
|
+
reason
|
|
312
|
+
};
|
|
313
|
+
} catch (err) {
|
|
314
|
+
const viemError = formatViemError(err);
|
|
315
|
+
// If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
|
|
316
|
+
// we can safely ignore it and return undefined so we go ahead with block building.
|
|
317
|
+
if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
|
|
318
|
+
this.log.verbose(`Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`, {
|
|
319
|
+
...logData,
|
|
320
|
+
request,
|
|
321
|
+
error: viemError.message
|
|
322
|
+
});
|
|
323
|
+
const latestPendingBlockNumber = await this.rollupContract.getBlockNumber();
|
|
324
|
+
if (latestPendingBlockNumber < blockNumber) {
|
|
325
|
+
this.log.verbose(`Block number ${blockNumber} has already been invalidated`, {
|
|
326
|
+
...logData
|
|
327
|
+
});
|
|
328
|
+
return undefined;
|
|
329
|
+
} else {
|
|
330
|
+
this.log.error(`Simulation for invalidate ${blockNumber} failed and it is still in pending chain`, viemError, logData);
|
|
331
|
+
throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
|
|
332
|
+
cause: viemError
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
|
|
337
|
+
this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
|
|
338
|
+
throw new Error(`Failed to simulate invalidate block ${blockNumber}`, {
|
|
339
|
+
cause: viemError
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
buildInvalidateBlockRequest(validationResult) {
|
|
344
|
+
if (validationResult.valid) {
|
|
345
|
+
throw new Error('Cannot invalidate a valid block');
|
|
346
|
+
}
|
|
347
|
+
const { block, committee, reason } = validationResult;
|
|
348
|
+
const logData = {
|
|
349
|
+
...block.block.toBlockInfo(),
|
|
350
|
+
reason
|
|
351
|
+
};
|
|
352
|
+
this.log.debug(`Simulating invalidate block ${block.block.number}`, logData);
|
|
353
|
+
if (reason === 'invalid-attestation') {
|
|
354
|
+
return this.rollupContract.buildInvalidateBadAttestationRequest(block.block.number, block.attestations.map((a)=>a.toViem()), committee, validationResult.invalidIndex);
|
|
355
|
+
} else if (reason === 'insufficient-attestations') {
|
|
356
|
+
return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(block.block.number, block.attestations.map((a)=>a.toViem()), committee);
|
|
357
|
+
} else {
|
|
358
|
+
const _ = reason;
|
|
359
|
+
throw new Error(`Unknown reason for invalidation`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
255
363
|
* @notice Will simulate `propose` to make sure that the block is valid for submission
|
|
256
364
|
*
|
|
257
365
|
* @dev Throws if unable to propose
|
|
@@ -262,7 +370,7 @@ export class SequencerPublisher {
|
|
|
262
370
|
*/ async validateBlockForSubmission(block, attestationData = {
|
|
263
371
|
digest: Buffer.alloc(32),
|
|
264
372
|
attestations: []
|
|
265
|
-
}) {
|
|
373
|
+
}, options) {
|
|
266
374
|
const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
|
|
267
375
|
// If we have no attestations, we still need to provide the empty attestations
|
|
268
376
|
// so that the committee is recalculated correctly
|
|
@@ -275,11 +383,10 @@ export class SequencerPublisher {
|
|
|
275
383
|
}
|
|
276
384
|
attestationData.attestations = committee.map((committeeMember)=>CommitteeAttestation.fromAddress(committeeMember));
|
|
277
385
|
}
|
|
278
|
-
// const blobs = await Blob.getBlobs(block.body.toBlobFields());
|
|
279
|
-
// const blobInput = Blob.getEthBlobEvaluationInputs(blobs);
|
|
280
386
|
const blobs = await Blob.getBlobsPerBlock(block.body.toBlobFields());
|
|
281
387
|
const blobInput = Blob.getPrefixedEthBlobCommitments(blobs);
|
|
282
388
|
const formattedAttestations = attestationData.attestations.map((attest)=>attest.toViem());
|
|
389
|
+
const signers = attestationData.attestations.filter((attest)=>!attest.signature.isEmpty()).map((attest)=>attest.address.toString());
|
|
283
390
|
const args = [
|
|
284
391
|
{
|
|
285
392
|
header: block.header.toPropose().toViem(),
|
|
@@ -291,37 +398,49 @@ export class SequencerPublisher {
|
|
|
291
398
|
}
|
|
292
399
|
},
|
|
293
400
|
RollupContract.packAttestations(formattedAttestations),
|
|
401
|
+
signers,
|
|
294
402
|
blobInput
|
|
295
403
|
];
|
|
296
|
-
await this.simulateProposeTx(args, ts);
|
|
404
|
+
await this.simulateProposeTx(args, ts, options);
|
|
297
405
|
return ts;
|
|
298
406
|
}
|
|
299
|
-
async
|
|
300
|
-
|
|
301
|
-
return committee?.map(EthAddress.fromString);
|
|
302
|
-
}
|
|
303
|
-
async enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base, signerAddress, signer) {
|
|
304
|
-
if (this.myLastVotes[voteType] >= slotNumber) {
|
|
407
|
+
async enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer) {
|
|
408
|
+
if (this.myLastSignals[signalType] >= slotNumber) {
|
|
305
409
|
return false;
|
|
306
410
|
}
|
|
307
411
|
if (payload.equals(EthAddress.ZERO)) {
|
|
308
412
|
return false;
|
|
309
413
|
}
|
|
414
|
+
if (signerAddress.equals(EthAddress.ZERO)) {
|
|
415
|
+
this.log.warn(`Cannot enqueue vote cast signal ${signalType} for address zero at slot ${slotNumber}`);
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
310
418
|
const round = await base.computeRound(slotNumber);
|
|
311
419
|
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
|
|
312
|
-
if (roundInfo.
|
|
420
|
+
if (roundInfo.lastSignalSlot >= slotNumber) {
|
|
313
421
|
return false;
|
|
314
422
|
}
|
|
315
|
-
const cachedLastVote = this.
|
|
316
|
-
this.
|
|
317
|
-
const action =
|
|
318
|
-
const request = await base.
|
|
423
|
+
const cachedLastVote = this.myLastSignals[signalType];
|
|
424
|
+
this.myLastSignals[signalType] = slotNumber;
|
|
425
|
+
const action = signalType === 0 ? 'governance-signal' : 'slashing-signal';
|
|
426
|
+
const request = await base.createSignalRequestWithSignature(payload.toString(), round, this.config.l1ChainId, signerAddress.toString(), signer);
|
|
319
427
|
this.log.debug(`Created ${action} request with signature`, {
|
|
320
428
|
request,
|
|
321
429
|
round,
|
|
322
430
|
signer: this.l1TxUtils.client.account?.address,
|
|
323
431
|
lastValidL2Slot: slotNumber
|
|
324
432
|
});
|
|
433
|
+
try {
|
|
434
|
+
await this.l1TxUtils.simulate(request, {
|
|
435
|
+
time: timestamp
|
|
436
|
+
}, [], ErrorsAbi);
|
|
437
|
+
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, {
|
|
438
|
+
request
|
|
439
|
+
});
|
|
440
|
+
} catch (err) {
|
|
441
|
+
this.log.warn(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
|
|
442
|
+
// Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
|
|
443
|
+
}
|
|
325
444
|
this.addRequest({
|
|
326
445
|
gasConfig: {
|
|
327
446
|
gasLimit: SequencerPublisher.VOTE_GAS_GUESS
|
|
@@ -329,23 +448,33 @@ export class SequencerPublisher {
|
|
|
329
448
|
action,
|
|
330
449
|
request,
|
|
331
450
|
lastValidL2Slot: slotNumber,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
451
|
+
checkSuccess: (_request, result)=>{
|
|
452
|
+
const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, base.address.toString(), EmpireBaseAbi, 'SignalCast');
|
|
453
|
+
const logData = {
|
|
454
|
+
...result,
|
|
455
|
+
slotNumber,
|
|
456
|
+
round,
|
|
457
|
+
payload: payload.toString()
|
|
458
|
+
};
|
|
459
|
+
if (!success) {
|
|
460
|
+
this.log.error(`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`, logData);
|
|
461
|
+
this.myLastSignals[signalType] = cachedLastVote;
|
|
462
|
+
return false;
|
|
335
463
|
} else {
|
|
336
|
-
this.log.info(`
|
|
464
|
+
this.log.info(`Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`, logData);
|
|
465
|
+
return true;
|
|
337
466
|
}
|
|
338
467
|
}
|
|
339
468
|
});
|
|
340
469
|
return true;
|
|
341
470
|
}
|
|
342
|
-
async
|
|
343
|
-
if (
|
|
471
|
+
async getSignalConfig(slotNumber, signalType) {
|
|
472
|
+
if (signalType === 0) {
|
|
344
473
|
return {
|
|
345
474
|
payload: this.governancePayload,
|
|
346
475
|
base: this.govProposerContract
|
|
347
476
|
};
|
|
348
|
-
} else if (
|
|
477
|
+
} else if (signalType === 1) {
|
|
349
478
|
if (!this.getSlashPayload) {
|
|
350
479
|
return undefined;
|
|
351
480
|
}
|
|
@@ -358,26 +487,24 @@ export class SequencerPublisher {
|
|
|
358
487
|
payload: slashPayload,
|
|
359
488
|
base: this.slashingProposerContract
|
|
360
489
|
};
|
|
490
|
+
} else {
|
|
491
|
+
const _ = signalType;
|
|
492
|
+
throw new Error('Unreachable: Invalid signal type');
|
|
361
493
|
}
|
|
362
|
-
throw new Error('Unreachable: Invalid vote type');
|
|
363
494
|
}
|
|
364
495
|
/**
|
|
365
|
-
* Enqueues a
|
|
366
|
-
* @param slotNumber - The slot number to cast a
|
|
367
|
-
* @param timestamp - The timestamp of the slot to cast a
|
|
368
|
-
* @param
|
|
369
|
-
* @returns True if the
|
|
370
|
-
*/ async
|
|
371
|
-
const
|
|
372
|
-
if (!
|
|
496
|
+
* Enqueues a castSignal transaction to cast a signal for a given slot number.
|
|
497
|
+
* @param slotNumber - The slot number to cast a signal for.
|
|
498
|
+
* @param timestamp - The timestamp of the slot to cast a signal for.
|
|
499
|
+
* @param signalType - The type of signal to cast.
|
|
500
|
+
* @returns True if the signal was successfully enqueued, false otherwise.
|
|
501
|
+
*/ async enqueueCastSignal(slotNumber, timestamp, signalType, signerAddress, signer) {
|
|
502
|
+
const signalConfig = await this.getSignalConfig(slotNumber, signalType);
|
|
503
|
+
if (!signalConfig) {
|
|
373
504
|
return false;
|
|
374
505
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
const { payload, base } = voteConfig;
|
|
380
|
-
return this.enqueueCastVoteHelper(slotNumber, timestamp, voteType, payload, base, signerAddress, signer);
|
|
506
|
+
const { payload, base } = signalConfig;
|
|
507
|
+
return this.enqueueCastSignalHelper(slotNumber, timestamp, signalType, payload, base, signerAddress, signer);
|
|
381
508
|
}
|
|
382
509
|
/**
|
|
383
510
|
* Proposes a L2 block on L1.
|
|
@@ -404,21 +531,64 @@ export class SequencerPublisher {
|
|
|
404
531
|
// This means that we can avoid the simulation issues in later checks.
|
|
405
532
|
// By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
|
|
406
533
|
// make time consistency checks break.
|
|
407
|
-
|
|
534
|
+
const attestationData = {
|
|
408
535
|
digest: digest.toBuffer(),
|
|
409
536
|
attestations: attestations ?? []
|
|
410
|
-
}
|
|
537
|
+
};
|
|
538
|
+
// TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
|
|
539
|
+
ts = await this.validateBlockForSubmission(block, attestationData, opts);
|
|
411
540
|
} catch (err) {
|
|
412
|
-
this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`,
|
|
541
|
+
this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
|
|
413
542
|
...block.getStats(),
|
|
414
|
-
slotNumber: block.header.globalVariables.slotNumber.toBigInt()
|
|
543
|
+
slotNumber: block.header.globalVariables.slotNumber.toBigInt(),
|
|
544
|
+
forcePendingBlockNumber: opts.forcePendingBlockNumber
|
|
415
545
|
});
|
|
416
546
|
throw err;
|
|
417
547
|
}
|
|
418
|
-
this.log.
|
|
548
|
+
this.log.verbose(`Enqueuing block propose transaction`, {
|
|
549
|
+
...block.toBlockInfo(),
|
|
550
|
+
...opts
|
|
551
|
+
});
|
|
419
552
|
await this.addProposeTx(block, proposeTxArgs, opts, ts);
|
|
420
553
|
return true;
|
|
421
554
|
}
|
|
555
|
+
enqueueInvalidateBlock(request, opts = {}) {
|
|
556
|
+
if (!request) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
|
|
560
|
+
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(request.gasUsed) * 64 / 63)));
|
|
561
|
+
const logData = {
|
|
562
|
+
...pick(request, 'gasUsed', 'blockNumber'),
|
|
563
|
+
gasLimit,
|
|
564
|
+
opts
|
|
565
|
+
};
|
|
566
|
+
this.log.verbose(`Enqueuing invalidate block request`, logData);
|
|
567
|
+
this.addRequest({
|
|
568
|
+
action: `invalidate-by-${request.reason}`,
|
|
569
|
+
request: request.request,
|
|
570
|
+
gasConfig: {
|
|
571
|
+
gasLimit,
|
|
572
|
+
txTimeoutAt: opts.txTimeoutAt
|
|
573
|
+
},
|
|
574
|
+
lastValidL2Slot: this.getCurrentL2Slot() + 2n,
|
|
575
|
+
checkSuccess: (_req, result)=>{
|
|
576
|
+
const success = result && result.receipt && result.receipt.status === 'success' && tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'BlockInvalidated');
|
|
577
|
+
if (!success) {
|
|
578
|
+
this.log.warn(`Invalidate block ${request.blockNumber} failed`, {
|
|
579
|
+
...result,
|
|
580
|
+
...logData
|
|
581
|
+
});
|
|
582
|
+
} else {
|
|
583
|
+
this.log.info(`Invalidate block ${request.blockNumber} succeeded`, {
|
|
584
|
+
...result,
|
|
585
|
+
...logData
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return !!success;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
422
592
|
/**
|
|
423
593
|
* Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
|
|
424
594
|
* Be warned, the call may return false even if the tx subsequently gets successfully mined.
|
|
@@ -432,7 +602,7 @@ export class SequencerPublisher {
|
|
|
432
602
|
this.interrupted = false;
|
|
433
603
|
this.l1TxUtils.restart();
|
|
434
604
|
}
|
|
435
|
-
async prepareProposeTx(encodedData, timestamp) {
|
|
605
|
+
async prepareProposeTx(encodedData, timestamp, options) {
|
|
436
606
|
if (!this.l1TxUtils.client.account) {
|
|
437
607
|
throw new Error('L1 TX utils needs to be initialized with an account wallet.');
|
|
438
608
|
}
|
|
@@ -462,6 +632,7 @@ export class SequencerPublisher {
|
|
|
462
632
|
});
|
|
463
633
|
const attestations = encodedData.attestations ? encodedData.attestations.map((attest)=>attest.toViem()) : [];
|
|
464
634
|
const txHashes = encodedData.txHashes ? encodedData.txHashes.map((txHash)=>txHash.toString()) : [];
|
|
635
|
+
const signers = encodedData.attestations?.filter((attest)=>!attest.signature.isEmpty()).map((attest)=>attest.address.toString());
|
|
465
636
|
const args = [
|
|
466
637
|
{
|
|
467
638
|
header: encodedData.header.toViem(),
|
|
@@ -474,9 +645,10 @@ export class SequencerPublisher {
|
|
|
474
645
|
txHashes
|
|
475
646
|
},
|
|
476
647
|
RollupContract.packAttestations(attestations),
|
|
648
|
+
signers ?? [],
|
|
477
649
|
blobInput
|
|
478
650
|
];
|
|
479
|
-
const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp);
|
|
651
|
+
const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
|
|
480
652
|
return {
|
|
481
653
|
args,
|
|
482
654
|
blobEvaluationGas,
|
|
@@ -489,28 +661,17 @@ export class SequencerPublisher {
|
|
|
489
661
|
* @param args - The propose tx args
|
|
490
662
|
* @param timestamp - The timestamp to simulate proposal at
|
|
491
663
|
* @returns The simulation result
|
|
492
|
-
*/ async simulateProposeTx(args, timestamp) {
|
|
664
|
+
*/ async simulateProposeTx(args, timestamp, options) {
|
|
493
665
|
const rollupData = encodeFunctionData({
|
|
494
666
|
abi: RollupAbi,
|
|
495
667
|
functionName: 'propose',
|
|
496
668
|
args
|
|
497
669
|
});
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
functionName: 'aggregate3',
|
|
501
|
-
args: [
|
|
502
|
-
[
|
|
503
|
-
{
|
|
504
|
-
target: this.rollupContract.address,
|
|
505
|
-
allowFailure: false,
|
|
506
|
-
callData: rollupData
|
|
507
|
-
}
|
|
508
|
-
]
|
|
509
|
-
]
|
|
510
|
-
});
|
|
670
|
+
// override the pending block number if requested
|
|
671
|
+
const forcePendingBlockNumberStateDiff = (options.forcePendingBlockNumber !== undefined ? await this.rollupContract.makePendingBlockNumberOverride(options.forcePendingBlockNumber) : []).flatMap((override)=>override.stateDiff ?? []);
|
|
511
672
|
const simulationResult = await this.l1TxUtils.simulate({
|
|
512
|
-
to:
|
|
513
|
-
data:
|
|
673
|
+
to: this.rollupContract.address,
|
|
674
|
+
data: rollupData,
|
|
514
675
|
gas: SequencerPublisher.PROPOSE_GAS_GUESS
|
|
515
676
|
}, {
|
|
516
677
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
@@ -525,7 +686,8 @@ export class SequencerPublisher {
|
|
|
525
686
|
{
|
|
526
687
|
slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
|
|
527
688
|
value: toPaddedHex(0n, true)
|
|
528
|
-
}
|
|
689
|
+
},
|
|
690
|
+
...forcePendingBlockNumberStateDiff
|
|
529
691
|
]
|
|
530
692
|
}
|
|
531
693
|
], RollupAbi, {
|
|
@@ -543,8 +705,14 @@ export class SequencerPublisher {
|
|
|
543
705
|
async addProposeTx(block, encodedData, opts = {}, timestamp) {
|
|
544
706
|
const timer = new Timer();
|
|
545
707
|
const kzg = Blob.getViemKzgInstance();
|
|
546
|
-
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp);
|
|
708
|
+
const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, timestamp, opts);
|
|
547
709
|
const startBlock = await this.l1TxUtils.getBlockNumber();
|
|
710
|
+
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil(Number(simulationResult.gasUsed) * 64 / 63)) + blobEvaluationGas + SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS);
|
|
711
|
+
// Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
|
|
712
|
+
// tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
|
|
713
|
+
void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch((_err)=>{
|
|
714
|
+
this.log.error('Failed to send blobs to blob sink');
|
|
715
|
+
});
|
|
548
716
|
return this.addRequest({
|
|
549
717
|
action: 'propose',
|
|
550
718
|
request: {
|
|
@@ -554,18 +722,19 @@ export class SequencerPublisher {
|
|
|
554
722
|
lastValidL2Slot: block.header.globalVariables.slotNumber.toBigInt(),
|
|
555
723
|
gasConfig: {
|
|
556
724
|
...opts,
|
|
557
|
-
gasLimit
|
|
725
|
+
gasLimit
|
|
558
726
|
},
|
|
559
727
|
blobConfig: {
|
|
560
728
|
blobs: encodedData.blobs.map((b)=>b.data),
|
|
561
729
|
kzg
|
|
562
730
|
},
|
|
563
|
-
|
|
731
|
+
checkSuccess: (request, result)=>{
|
|
564
732
|
if (!result) {
|
|
565
|
-
return;
|
|
733
|
+
return false;
|
|
566
734
|
}
|
|
567
735
|
const { receipt, stats, errorMsg } = result;
|
|
568
|
-
|
|
736
|
+
const success = receipt && receipt.status === 'success' && tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'L2BlockProposed');
|
|
737
|
+
if (success) {
|
|
569
738
|
const endBlock = receipt.blockNumber;
|
|
570
739
|
const inclusionBlocks = Number(endBlock - startBlock);
|
|
571
740
|
const publishStats = {
|
|
@@ -580,35 +749,24 @@ export class SequencerPublisher {
|
|
|
580
749
|
blobCount: encodedData.blobs.length,
|
|
581
750
|
inclusionBlocks
|
|
582
751
|
};
|
|
583
|
-
this.log.
|
|
752
|
+
this.log.info(`Published L2 block to L1 rollup contract`, {
|
|
584
753
|
...stats,
|
|
585
|
-
...block.getStats()
|
|
754
|
+
...block.getStats(),
|
|
755
|
+
...receipt
|
|
586
756
|
});
|
|
587
757
|
this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
|
|
588
|
-
// Send the blobs to the blob sink
|
|
589
|
-
this.sendBlobsToBlobSink(receipt.blockHash, encodedData.blobs).catch((_err)=>{
|
|
590
|
-
this.log.error('Failed to send blobs to blob sink');
|
|
591
|
-
});
|
|
592
758
|
return true;
|
|
593
759
|
} else {
|
|
594
760
|
this.metrics.recordFailedTx('process');
|
|
595
|
-
this.log.error(`Rollup process tx
|
|
761
|
+
this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
|
|
596
762
|
...block.getStats(),
|
|
763
|
+
receipt,
|
|
597
764
|
txHash: receipt.transactionHash,
|
|
598
765
|
slotNumber: block.header.globalVariables.slotNumber.toBigInt()
|
|
599
766
|
});
|
|
767
|
+
return false;
|
|
600
768
|
}
|
|
601
769
|
}
|
|
602
770
|
});
|
|
603
771
|
}
|
|
604
|
-
/**
|
|
605
|
-
* Send blobs to the blob sink
|
|
606
|
-
*
|
|
607
|
-
* If a blob sink url is configured, then we send blobs to the blob sink
|
|
608
|
-
* - for now we use the blockHash as the identifier for the blobs;
|
|
609
|
-
* In the future this will move to be the beacon block id - which takes a bit more work
|
|
610
|
-
* to calculate and will need to be mocked in e2e tests
|
|
611
|
-
*/ sendBlobsToBlobSink(blockHash, blobs) {
|
|
612
|
-
return this.blobSinkClient.sendBlobsToBlobSink(blockHash, blobs);
|
|
613
|
-
}
|
|
614
772
|
}
|