@aztec/sequencer-client 0.0.1-commit.1bea0213 → 0.0.1-commit.21ecf947b
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.js +1 -1
- package/dest/config.d.ts +1 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +4 -8
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +8 -2
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +53 -23
- package/dest/sequencer/checkpoint_proposal_job.d.ts +32 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +79 -51
- package/dest/sequencer/metrics.d.ts +6 -2
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +81 -22
- package/dest/sequencer/sequencer.d.ts +3 -1
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +7 -2
- package/dest/sequencer/timetable.d.ts +1 -4
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +1 -4
- package/dest/test/mock_checkpoint_builder.d.ts +7 -5
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +6 -6
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +1 -1
- package/src/config.ts +9 -11
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +60 -22
- package/src/sequencer/checkpoint_proposal_job.ts +104 -67
- package/src/sequencer/metrics.ts +89 -23
- package/src/sequencer/sequencer.ts +9 -2
- package/src/sequencer/timetable.ts +6 -5
- package/src/test/mock_checkpoint_builder.ts +14 -5
- package/src/test/utils.ts +5 -2
|
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
4
4
|
import type { L1ContractsConfig } from '@aztec/ethereum/config';
|
|
5
5
|
import {
|
|
6
6
|
type EmpireSlashingProposerContract,
|
|
7
|
+
FeeAssetPriceOracle,
|
|
7
8
|
type GovernanceProposerContract,
|
|
8
9
|
type IEmpireBase,
|
|
9
10
|
MULTI_CALL_3_ADDRESS,
|
|
@@ -18,11 +19,12 @@ import {
|
|
|
18
19
|
type L1BlobInputs,
|
|
19
20
|
type L1TxConfig,
|
|
20
21
|
type L1TxRequest,
|
|
22
|
+
MAX_L1_TX_LIMIT,
|
|
21
23
|
type TransactionStats,
|
|
22
24
|
WEI_CONST,
|
|
23
25
|
} from '@aztec/ethereum/l1-tx-utils';
|
|
24
26
|
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
25
|
-
import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/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
30
|
import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
@@ -59,6 +61,8 @@ type L1ProcessArgs = {
|
|
|
59
61
|
attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
60
62
|
/** Attestations and signers signature */
|
|
61
63
|
attestationsAndSignersSignature: Signature;
|
|
64
|
+
/** The fee asset price modifier in basis points (from oracle) */
|
|
65
|
+
feeAssetPriceModifier: bigint;
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
export const Actions = [
|
|
@@ -122,10 +126,9 @@ export class SequencerPublisher {
|
|
|
122
126
|
|
|
123
127
|
/** L1 fee analyzer for fisherman mode */
|
|
124
128
|
private l1FeeAnalyzer?: L1FeeAnalyzer;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
|
|
129
|
+
|
|
130
|
+
/** Fee asset price oracle for computing price modifiers from Uniswap V4 */
|
|
131
|
+
private feeAssetPriceOracle: FeeAssetPriceOracle;
|
|
129
132
|
|
|
130
133
|
// A CALL to a cold address is 2700 gas
|
|
131
134
|
public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
|
|
@@ -192,12 +195,27 @@ export class SequencerPublisher {
|
|
|
192
195
|
createLogger('sequencer:publisher:fee-analyzer'),
|
|
193
196
|
);
|
|
194
197
|
}
|
|
198
|
+
|
|
199
|
+
// Initialize fee asset price oracle
|
|
200
|
+
this.feeAssetPriceOracle = new FeeAssetPriceOracle(
|
|
201
|
+
this.l1TxUtils.client,
|
|
202
|
+
this.rollupContract,
|
|
203
|
+
createLogger('sequencer:publisher:price-oracle'),
|
|
204
|
+
);
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
public getRollupContract(): RollupContract {
|
|
198
208
|
return this.rollupContract;
|
|
199
209
|
}
|
|
200
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Gets the fee asset price modifier from the oracle.
|
|
213
|
+
* Returns 0n if the oracle query fails.
|
|
214
|
+
*/
|
|
215
|
+
public getFeeAssetPriceModifier(): Promise<bigint> {
|
|
216
|
+
return this.feeAssetPriceOracle.computePriceModifier();
|
|
217
|
+
}
|
|
218
|
+
|
|
201
219
|
public getSenderAddress() {
|
|
202
220
|
return this.l1TxUtils.getSenderAddress();
|
|
203
221
|
}
|
|
@@ -273,7 +291,7 @@ export class SequencerPublisher {
|
|
|
273
291
|
// Start the analysis
|
|
274
292
|
const analysisId = await this.l1FeeAnalyzer.startAnalysis(
|
|
275
293
|
l2SlotNumber,
|
|
276
|
-
gasLimit > 0n ? gasLimit :
|
|
294
|
+
gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
|
|
277
295
|
l1Requests,
|
|
278
296
|
blobConfig,
|
|
279
297
|
onComplete,
|
|
@@ -346,7 +364,16 @@ export class SequencerPublisher {
|
|
|
346
364
|
|
|
347
365
|
// Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
|
|
348
366
|
const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
|
|
349
|
-
|
|
367
|
+
let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
|
|
368
|
+
// Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
|
|
369
|
+
const maxGas = MAX_L1_TX_LIMIT;
|
|
370
|
+
if (gasLimit !== undefined && gasLimit > maxGas) {
|
|
371
|
+
this.log.debug('Capping bundled tx gas limit to L1 max', {
|
|
372
|
+
requested: gasLimit,
|
|
373
|
+
capped: maxGas,
|
|
374
|
+
});
|
|
375
|
+
gasLimit = maxGas;
|
|
376
|
+
}
|
|
350
377
|
const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
|
|
351
378
|
const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
|
|
352
379
|
const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
|
|
@@ -517,7 +544,12 @@ export class SequencerPublisher {
|
|
|
517
544
|
this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
|
|
518
545
|
|
|
519
546
|
try {
|
|
520
|
-
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
547
|
+
const { gasUsed } = await this.l1TxUtils.simulate(
|
|
548
|
+
request,
|
|
549
|
+
undefined,
|
|
550
|
+
undefined,
|
|
551
|
+
mergeAbis([request.abi ?? [], ErrorsAbi]),
|
|
552
|
+
);
|
|
521
553
|
this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
|
|
522
554
|
...logData,
|
|
523
555
|
request,
|
|
@@ -536,7 +568,7 @@ export class SequencerPublisher {
|
|
|
536
568
|
|
|
537
569
|
// If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
|
|
538
570
|
// we can safely ignore it and return undefined so we go ahead with checkpoint building.
|
|
539
|
-
if (viemError.message?.includes('
|
|
571
|
+
if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
|
|
540
572
|
this.log.verbose(
|
|
541
573
|
`Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
|
|
542
574
|
{ ...logData, request, error: viemError.message },
|
|
@@ -632,7 +664,7 @@ export class SequencerPublisher {
|
|
|
632
664
|
header: checkpoint.header.toViem(),
|
|
633
665
|
archive: toHex(checkpoint.archive.root.toBuffer()),
|
|
634
666
|
oracleInput: {
|
|
635
|
-
feeAssetPriceModifier:
|
|
667
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
636
668
|
},
|
|
637
669
|
},
|
|
638
670
|
attestationsAndSigners.getPackedAttestations(),
|
|
@@ -700,7 +732,7 @@ export class SequencerPublisher {
|
|
|
700
732
|
});
|
|
701
733
|
|
|
702
734
|
try {
|
|
703
|
-
await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
|
|
735
|
+
await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
|
|
704
736
|
this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
|
|
705
737
|
} catch (err) {
|
|
706
738
|
this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
|
|
@@ -910,12 +942,13 @@ export class SequencerPublisher {
|
|
|
910
942
|
const blobFields = checkpoint.toBlobFields();
|
|
911
943
|
const blobs = getBlobsPerL1Block(blobFields);
|
|
912
944
|
|
|
913
|
-
const proposeTxArgs = {
|
|
945
|
+
const proposeTxArgs: L1ProcessArgs = {
|
|
914
946
|
header: checkpointHeader,
|
|
915
947
|
archive: checkpoint.archive.root.toBuffer(),
|
|
916
948
|
blobs,
|
|
917
949
|
attestationsAndSigners,
|
|
918
950
|
attestationsAndSignersSignature,
|
|
951
|
+
feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
|
|
919
952
|
};
|
|
920
953
|
|
|
921
954
|
let ts: bigint;
|
|
@@ -999,12 +1032,14 @@ export class SequencerPublisher {
|
|
|
999
1032
|
this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
|
|
1000
1033
|
|
|
1001
1034
|
let gasUsed: bigint;
|
|
1035
|
+
const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
|
|
1002
1036
|
try {
|
|
1003
|
-
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [],
|
|
1037
|
+
({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
|
|
1004
1038
|
this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
|
|
1005
1039
|
} catch (err) {
|
|
1006
|
-
const viemError = formatViemError(err);
|
|
1040
|
+
const viemError = formatViemError(err, simulateAbi);
|
|
1007
1041
|
this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
|
|
1042
|
+
|
|
1008
1043
|
return false;
|
|
1009
1044
|
}
|
|
1010
1045
|
|
|
@@ -1012,10 +1047,14 @@ export class SequencerPublisher {
|
|
|
1012
1047
|
const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
|
|
1013
1048
|
logData.gasLimit = gasLimit;
|
|
1014
1049
|
|
|
1050
|
+
// Store the ABI used for simulation on the request so Multicall3.forward can decode errors
|
|
1051
|
+
// when the tx is sent and a revert is diagnosed via simulation.
|
|
1052
|
+
const requestWithAbi = { ...request, abi: simulateAbi };
|
|
1053
|
+
|
|
1015
1054
|
this.log.debug(`Enqueuing ${action}`, logData);
|
|
1016
1055
|
this.addRequest({
|
|
1017
1056
|
action,
|
|
1018
|
-
request,
|
|
1057
|
+
request: requestWithAbi,
|
|
1019
1058
|
gasConfig: { gasLimit },
|
|
1020
1059
|
lastValidL2Slot: slotNumber,
|
|
1021
1060
|
checkSuccess: (_req, result) => {
|
|
@@ -1097,8 +1136,7 @@ export class SequencerPublisher {
|
|
|
1097
1136
|
header: encodedData.header.toViem(),
|
|
1098
1137
|
archive: toHex(encodedData.archive),
|
|
1099
1138
|
oracleInput: {
|
|
1100
|
-
|
|
1101
|
-
feeAssetPriceModifier: 0n,
|
|
1139
|
+
feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
|
|
1102
1140
|
},
|
|
1103
1141
|
},
|
|
1104
1142
|
encodedData.attestationsAndSigners.getPackedAttestations(),
|
|
@@ -1124,7 +1162,7 @@ export class SequencerPublisher {
|
|
|
1124
1162
|
readonly header: ViemHeader;
|
|
1125
1163
|
readonly archive: `0x${string}`;
|
|
1126
1164
|
readonly oracleInput: {
|
|
1127
|
-
readonly feeAssetPriceModifier:
|
|
1165
|
+
readonly feeAssetPriceModifier: bigint;
|
|
1128
1166
|
};
|
|
1129
1167
|
},
|
|
1130
1168
|
ViemCommitteeAttestations,
|
|
@@ -1171,20 +1209,20 @@ export class SequencerPublisher {
|
|
|
1171
1209
|
{
|
|
1172
1210
|
to: this.rollupContract.address,
|
|
1173
1211
|
data: rollupData,
|
|
1174
|
-
gas:
|
|
1212
|
+
gas: MAX_L1_TX_LIMIT,
|
|
1175
1213
|
...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
|
|
1176
1214
|
},
|
|
1177
1215
|
{
|
|
1178
1216
|
// @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
|
|
1179
1217
|
time: timestamp + 1n,
|
|
1180
1218
|
// @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
|
|
1181
|
-
gasLimit:
|
|
1219
|
+
gasLimit: MAX_L1_TX_LIMIT * 2n,
|
|
1182
1220
|
},
|
|
1183
1221
|
stateOverrides,
|
|
1184
1222
|
RollupAbi,
|
|
1185
1223
|
{
|
|
1186
1224
|
// @note fallback gas estimate to use if the node doesn't support simulation API
|
|
1187
|
-
fallbackGasEstimate:
|
|
1225
|
+
fallbackGasEstimate: MAX_L1_TX_LIMIT,
|
|
1188
1226
|
},
|
|
1189
1227
|
)
|
|
1190
1228
|
.catch(err => {
|
|
@@ -1194,7 +1232,7 @@ export class SequencerPublisher {
|
|
|
1194
1232
|
this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
|
|
1195
1233
|
// Return a minimal simulation result with the fallback gas estimate
|
|
1196
1234
|
return {
|
|
1197
|
-
gasUsed:
|
|
1235
|
+
gasUsed: MAX_L1_TX_LIMIT,
|
|
1198
1236
|
logs: [],
|
|
1199
1237
|
};
|
|
1200
1238
|
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
2
|
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BlockNumber,
|
|
6
|
+
CheckpointNumber,
|
|
7
|
+
EpochNumber,
|
|
8
|
+
IndexWithinCheckpoint,
|
|
9
|
+
SlotNumber,
|
|
10
|
+
} from '@aztec/foundation/branded-types';
|
|
5
11
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
6
12
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
7
13
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
14
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
9
15
|
import { filter } from '@aztec/foundation/iterator';
|
|
10
|
-
import type
|
|
16
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
11
17
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
12
18
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
13
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
19
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
14
20
|
import type { P2P } from '@aztec/p2p';
|
|
15
21
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
16
22
|
import {
|
|
@@ -24,10 +30,11 @@ import {
|
|
|
24
30
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
25
31
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
26
32
|
import { Gas } from '@aztec/stdlib/gas';
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
import {
|
|
34
|
+
NoValidTxsError,
|
|
35
|
+
type PublicProcessorLimits,
|
|
36
|
+
type ResolvedSequencerConfig,
|
|
37
|
+
type WorldStateSynchronizer,
|
|
31
38
|
} from '@aztec/stdlib/interfaces/server';
|
|
32
39
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
33
40
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
@@ -59,6 +66,8 @@ const TXS_POLLING_MS = 500;
|
|
|
59
66
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
60
67
|
*/
|
|
61
68
|
export class CheckpointProposalJob implements Traceable {
|
|
69
|
+
protected readonly log: Logger;
|
|
70
|
+
|
|
62
71
|
constructor(
|
|
63
72
|
private readonly epoch: EpochNumber,
|
|
64
73
|
private readonly slot: SlotNumber,
|
|
@@ -86,9 +95,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
86
95
|
private readonly metrics: SequencerMetrics,
|
|
87
96
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
88
97
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
89
|
-
protected readonly log: Logger,
|
|
90
98
|
public readonly tracer: Tracer,
|
|
91
|
-
|
|
99
|
+
bindings?: LoggerBindings,
|
|
100
|
+
) {
|
|
101
|
+
this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
|
|
102
|
+
}
|
|
92
103
|
|
|
93
104
|
/**
|
|
94
105
|
* Executes the checkpoint proposal job.
|
|
@@ -180,6 +191,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
180
191
|
);
|
|
181
192
|
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
182
193
|
|
|
194
|
+
// Get the fee asset price modifier from the oracle
|
|
195
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
196
|
+
|
|
183
197
|
// Create a long-lived forked world state for the checkpoint builder
|
|
184
198
|
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
185
199
|
|
|
@@ -187,9 +201,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
187
201
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
188
202
|
this.checkpointNumber,
|
|
189
203
|
checkpointGlobalVariables,
|
|
204
|
+
feeAssetPriceModifier,
|
|
190
205
|
l1ToL2Messages,
|
|
191
206
|
previousCheckpointOutHashes,
|
|
192
207
|
fork,
|
|
208
|
+
this.log.getBindings(),
|
|
193
209
|
);
|
|
194
210
|
|
|
195
211
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -220,19 +236,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
220
236
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
221
237
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
222
238
|
// which is normal for block building (may have picked different txs)
|
|
223
|
-
if (err
|
|
224
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
225
|
-
slot: this.slot,
|
|
226
|
-
signedByNode: err.signedByNode,
|
|
227
|
-
});
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
if (err instanceof SlashingProtectionError) {
|
|
231
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
232
|
-
slot: this.slot,
|
|
233
|
-
existingMessageHash: err.existingMessageHash,
|
|
234
|
-
attemptedMessageHash: err.attemptedMessageHash,
|
|
235
|
-
});
|
|
239
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
236
240
|
return undefined;
|
|
237
241
|
}
|
|
238
242
|
throw err;
|
|
@@ -275,6 +279,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
275
279
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
280
|
checkpoint.header,
|
|
277
281
|
checkpoint.archive.root,
|
|
282
|
+
feeAssetPriceModifier,
|
|
278
283
|
lastBlock,
|
|
279
284
|
this.proposer,
|
|
280
285
|
checkpointProposalOptions,
|
|
@@ -301,20 +306,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
301
306
|
);
|
|
302
307
|
} catch (err) {
|
|
303
308
|
// We shouldn't really get here since we yield to another HA node
|
|
304
|
-
// as soon as we see these errors when creating block proposals.
|
|
305
|
-
if (err
|
|
306
|
-
this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
307
|
-
slot: this.slot,
|
|
308
|
-
signedByNode: err.signedByNode,
|
|
309
|
-
});
|
|
310
|
-
return undefined;
|
|
311
|
-
}
|
|
312
|
-
if (err instanceof SlashingProtectionError) {
|
|
313
|
-
this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
314
|
-
slot: this.slot,
|
|
315
|
-
existingMessageHash: err.existingMessageHash,
|
|
316
|
-
attemptedMessageHash: err.attemptedMessageHash,
|
|
317
|
-
});
|
|
309
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
310
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
318
311
|
return undefined;
|
|
319
312
|
}
|
|
320
313
|
throw err;
|
|
@@ -367,7 +360,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
367
360
|
|
|
368
361
|
while (true) {
|
|
369
362
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
370
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
363
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
371
364
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
372
365
|
|
|
373
366
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
@@ -397,6 +390,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
397
390
|
remainingBlobFields,
|
|
398
391
|
});
|
|
399
392
|
|
|
393
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
400
394
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
401
395
|
// If no block was produced due to not enough txs and this was the last subslot, exit
|
|
402
396
|
break;
|
|
@@ -483,13 +477,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
483
477
|
|
|
484
478
|
/** Builds a single block. Called from the main block building loop. */
|
|
485
479
|
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
486
|
-
|
|
480
|
+
protected async buildSingleBlock(
|
|
487
481
|
checkpointBuilder: CheckpointBuilder,
|
|
488
482
|
opts: {
|
|
489
483
|
forceCreate?: boolean;
|
|
490
484
|
blockTimestamp: bigint;
|
|
491
485
|
blockNumber: BlockNumber;
|
|
492
|
-
indexWithinCheckpoint:
|
|
486
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
493
487
|
buildDeadline: Date | undefined;
|
|
494
488
|
txHashesAlreadyIncluded: Set<string>;
|
|
495
489
|
remainingBlobFields: number;
|
|
@@ -527,7 +521,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
527
521
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
528
522
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
529
523
|
const pendingTxs = filter(
|
|
530
|
-
this.p2pClient.
|
|
524
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
531
525
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
532
526
|
);
|
|
533
527
|
|
|
@@ -550,45 +544,38 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
550
544
|
};
|
|
551
545
|
|
|
552
546
|
// Actually build the block by executing txs
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
usedTxs,
|
|
561
|
-
failedTxs,
|
|
562
|
-
usedTxBlobFields,
|
|
563
|
-
} = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
564
|
-
const blockBuildDuration = workTimer.ms();
|
|
547
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
548
|
+
checkpointBuilder,
|
|
549
|
+
pendingTxs,
|
|
550
|
+
blockNumber,
|
|
551
|
+
blockTimestamp,
|
|
552
|
+
blockBuilderOptions,
|
|
553
|
+
);
|
|
565
554
|
|
|
566
555
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
567
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
556
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
568
557
|
|
|
569
558
|
// Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
|
|
570
559
|
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
571
560
|
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
572
|
-
|
|
561
|
+
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
562
|
+
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
573
563
|
this.log.warn(
|
|
574
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed
|
|
575
|
-
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
|
|
564
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
565
|
+
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
|
|
576
566
|
);
|
|
577
|
-
this.eventEmitter.emit('block-
|
|
578
|
-
minTxs: minValidTxs,
|
|
579
|
-
availableTxs: numTxs,
|
|
580
|
-
slot: this.slot,
|
|
581
|
-
});
|
|
567
|
+
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
582
568
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
583
569
|
return undefined;
|
|
584
570
|
}
|
|
585
571
|
|
|
586
572
|
// Block creation succeeded, emit stats and metrics
|
|
573
|
+
const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
|
|
574
|
+
|
|
587
575
|
const blockStats = {
|
|
588
576
|
eventName: 'l2-block-built',
|
|
589
577
|
duration: blockBuildDuration,
|
|
590
578
|
publicProcessDuration: publicProcessorDuration,
|
|
591
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
592
579
|
...block.getStats(),
|
|
593
580
|
} satisfies L2BlockBuiltStats;
|
|
594
581
|
|
|
@@ -614,17 +601,40 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
614
601
|
}
|
|
615
602
|
}
|
|
616
603
|
|
|
604
|
+
/** Uses the checkpoint builder to build a block, catching specific txs */
|
|
605
|
+
private async buildSingleBlockWithCheckpointBuilder(
|
|
606
|
+
checkpointBuilder: CheckpointBuilder,
|
|
607
|
+
pendingTxs: AsyncIterable<Tx>,
|
|
608
|
+
blockNumber: BlockNumber,
|
|
609
|
+
blockTimestamp: bigint,
|
|
610
|
+
blockBuilderOptions: PublicProcessorLimits,
|
|
611
|
+
) {
|
|
612
|
+
try {
|
|
613
|
+
const workTimer = new Timer();
|
|
614
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
615
|
+
const blockBuildDuration = workTimer.ms();
|
|
616
|
+
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
617
|
+
} catch (err: unknown) {
|
|
618
|
+
if (isErrorClass(err, NoValidTxsError)) {
|
|
619
|
+
return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
|
|
620
|
+
}
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
617
625
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
618
626
|
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
619
627
|
private async waitForMinTxs(opts: {
|
|
620
628
|
forceCreate?: boolean;
|
|
621
629
|
blockNumber: BlockNumber;
|
|
622
|
-
indexWithinCheckpoint:
|
|
630
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
623
631
|
buildDeadline: Date | undefined;
|
|
624
632
|
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
625
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
626
633
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
627
634
|
|
|
635
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
636
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
637
|
+
|
|
628
638
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
629
639
|
const startBuildingDeadline = buildDeadline
|
|
630
640
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -645,7 +655,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
645
655
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
646
656
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
647
657
|
);
|
|
648
|
-
await
|
|
658
|
+
await this.waitForTxsPollingInterval();
|
|
649
659
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
650
660
|
}
|
|
651
661
|
|
|
@@ -686,7 +696,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
686
696
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
687
697
|
? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
|
|
688
698
|
: this.l1Constants.slotDuration;
|
|
689
|
-
const attestationDeadline = new Date(this.
|
|
699
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
690
700
|
|
|
691
701
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
692
702
|
|
|
@@ -774,7 +784,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
774
784
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
775
785
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
776
786
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
777
|
-
await this.p2pClient.
|
|
787
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
778
788
|
}
|
|
779
789
|
|
|
780
790
|
/**
|
|
@@ -822,6 +832,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
822
832
|
this.publisher.clearPendingRequests();
|
|
823
833
|
}
|
|
824
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
837
|
+
*/
|
|
838
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
839
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
840
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
841
|
+
slot: this.slot,
|
|
842
|
+
signedByNode: err.signedByNode,
|
|
843
|
+
});
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
if (err instanceof SlashingProtectionError) {
|
|
847
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
848
|
+
slot: this.slot,
|
|
849
|
+
existingMessageHash: err.existingMessageHash,
|
|
850
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
851
|
+
});
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
|
|
825
857
|
/** Waits until a specific time within the current slot */
|
|
826
858
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
827
859
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -830,6 +862,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
830
862
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
831
863
|
}
|
|
832
864
|
|
|
865
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
866
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
867
|
+
await sleep(TXS_POLLING_MS);
|
|
868
|
+
}
|
|
869
|
+
|
|
833
870
|
private getSlotStartBuildTimestamp(): number {
|
|
834
871
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
835
872
|
}
|