@aztec/sequencer-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec7ac5448
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 +17 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +63 -30
- package/dest/config.d.ts +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +47 -33
- package/dest/global_variable_builder/global_builder.d.ts +15 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +26 -26
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -3
- 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 +75 -49
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +470 -156
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +51 -13
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +414 -239
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +25 -6
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +134 -31
- package/dest/sequencer/sequencer.d.ts +44 -17
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +157 -94
- package/dest/sequencer/timetable.d.ts +17 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -46
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +14 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +63 -40
- 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 +27 -28
- package/src/client/sequencer-client.ts +81 -30
- package/src/config.ts +65 -43
- package/src/global_variable_builder/global_builder.ts +31 -28
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +153 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -9
- package/src/publisher/sequencer-publisher-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +531 -214
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +547 -274
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +153 -33
- package/src/sequencer/sequencer.ts +228 -107
- package/src/sequencer/timetable.ts +70 -57
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +84 -56
- package/src/test/utils.ts +5 -2
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
2
|
+
import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
|
|
3
|
+
import {
|
|
4
|
+
BlockNumber,
|
|
5
|
+
CheckpointNumber,
|
|
6
|
+
EpochNumber,
|
|
7
|
+
IndexWithinCheckpoint,
|
|
8
|
+
SlotNumber,
|
|
9
|
+
} from '@aztec/foundation/branded-types';
|
|
5
10
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
11
|
+
import {
|
|
12
|
+
flipSignature,
|
|
13
|
+
generateRecoverableSignature,
|
|
14
|
+
generateUnrecoverableSignature,
|
|
15
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
6
16
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
7
17
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
18
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
9
19
|
import { filter } from '@aztec/foundation/iterator';
|
|
10
|
-
import type
|
|
20
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
11
21
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
12
22
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
13
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
23
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
14
24
|
import type { P2P } from '@aztec/p2p';
|
|
15
25
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
16
26
|
import {
|
|
@@ -21,17 +31,23 @@ import {
|
|
|
21
31
|
type L2BlockSource,
|
|
22
32
|
MaliciousCommitteeAttestationsAndSigners,
|
|
23
33
|
} from '@aztec/stdlib/block';
|
|
24
|
-
import type
|
|
25
|
-
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
34
|
+
import { type Checkpoint, type ProposedCheckpointData, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
35
|
+
import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
26
36
|
import { Gas } from '@aztec/stdlib/gas';
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
import {
|
|
38
|
+
type BlockBuilderOptions,
|
|
39
|
+
InsufficientValidTxsError,
|
|
40
|
+
type ResolvedSequencerConfig,
|
|
41
|
+
type WorldStateSynchronizer,
|
|
31
42
|
} from '@aztec/stdlib/interfaces/server';
|
|
32
43
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
33
|
-
import type {
|
|
34
|
-
|
|
44
|
+
import type {
|
|
45
|
+
BlockProposal,
|
|
46
|
+
BlockProposalOptions,
|
|
47
|
+
CheckpointProposal,
|
|
48
|
+
CheckpointProposalOptions,
|
|
49
|
+
} from '@aztec/stdlib/p2p';
|
|
50
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
35
51
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
36
52
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
37
53
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -41,6 +57,10 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
41
57
|
|
|
42
58
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
43
59
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
60
|
+
import {
|
|
61
|
+
buildPipelinedParentSimulationOverridesPlan,
|
|
62
|
+
buildSubmissionSimulationOverridesPlan,
|
|
63
|
+
} from './chain_state_overrides.js';
|
|
44
64
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
45
65
|
import { SequencerInterruptedError } from './errors.js';
|
|
46
66
|
import type { SequencerEvents } from './events.js';
|
|
@@ -52,6 +72,20 @@ import { SequencerState } from './utils.js';
|
|
|
52
72
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
53
73
|
const TXS_POLLING_MS = 500;
|
|
54
74
|
|
|
75
|
+
/** Result from proposeCheckpoint when a checkpoint was successfully built and broadcast. */
|
|
76
|
+
type CheckpointProposalBroadcast = {
|
|
77
|
+
checkpoint: Checkpoint;
|
|
78
|
+
proposal: CheckpointProposal;
|
|
79
|
+
blockProposedAt: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Result after attestation collection and signing, ready for L1 submission. */
|
|
83
|
+
type CheckpointProposalResult = {
|
|
84
|
+
checkpoint: Checkpoint;
|
|
85
|
+
attestations: CommitteeAttestationsAndSigners;
|
|
86
|
+
attestationsSignature: Signature;
|
|
87
|
+
};
|
|
88
|
+
|
|
55
89
|
/**
|
|
56
90
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
57
91
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -59,9 +93,18 @@ const TXS_POLLING_MS = 500;
|
|
|
59
93
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
60
94
|
*/
|
|
61
95
|
export class CheckpointProposalJob implements Traceable {
|
|
96
|
+
protected readonly log: Logger;
|
|
97
|
+
|
|
98
|
+
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
99
|
+
private pendingL1Submission: Promise<void> | undefined;
|
|
100
|
+
|
|
101
|
+
/** Pipelined parent chain state used while building and later submitting this checkpoint. */
|
|
102
|
+
private pipelinedParentSimulationOverridesPlan?: SimulationOverridesPlan;
|
|
103
|
+
|
|
62
104
|
constructor(
|
|
63
|
-
private readonly
|
|
64
|
-
private readonly
|
|
105
|
+
private readonly slotNow: SlotNumber,
|
|
106
|
+
private readonly targetSlot: SlotNumber,
|
|
107
|
+
private readonly targetEpoch: EpochNumber,
|
|
65
108
|
private readonly checkpointNumber: CheckpointNumber,
|
|
66
109
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
67
110
|
// TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
@@ -86,13 +129,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
86
129
|
private readonly metrics: SequencerMetrics,
|
|
87
130
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
88
131
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
89
|
-
protected readonly log: Logger,
|
|
90
132
|
public readonly tracer: Tracer,
|
|
91
|
-
|
|
133
|
+
bindings?: LoggerBindings,
|
|
134
|
+
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
135
|
+
) {
|
|
136
|
+
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
137
|
+
...bindings,
|
|
138
|
+
instanceId: `slot-${this.slotNow}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
143
|
+
public async awaitPendingSubmission(): Promise<void> {
|
|
144
|
+
this.log.info('Awaiting pending L1 payload submission');
|
|
145
|
+
await this.pendingL1Submission;
|
|
146
|
+
}
|
|
92
147
|
|
|
93
148
|
/**
|
|
94
149
|
* Executes the checkpoint proposal job.
|
|
95
|
-
*
|
|
150
|
+
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
151
|
+
* Attestation collection, signing, and L1 submission are backgrounded so the
|
|
152
|
+
* work loop can return to IDLE immediately for consecutive slot proposals.
|
|
153
|
+
* Returns the built checkpoint if successful, undefined otherwise.
|
|
96
154
|
*/
|
|
97
155
|
@trackSpan('CheckpointProposalJob.execute')
|
|
98
156
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
@@ -100,7 +158,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
100
158
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
101
159
|
// These are constant for the whole slot, so we only enqueue them once
|
|
102
160
|
const votesPromises = new CheckpointVoter(
|
|
103
|
-
this.
|
|
161
|
+
this.targetSlot,
|
|
104
162
|
this.publisher,
|
|
105
163
|
this.attestorAddress,
|
|
106
164
|
this.validatorClient,
|
|
@@ -111,63 +169,185 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
111
169
|
this.log,
|
|
112
170
|
).enqueueVotes();
|
|
113
171
|
|
|
114
|
-
// Build
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
118
|
-
await Promise.all(votesPromises);
|
|
172
|
+
// Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
|
|
173
|
+
// Returns after broadcast — attestation collection is deferred.
|
|
174
|
+
const broadcast = await this.proposeCheckpoint();
|
|
119
175
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
176
|
+
if (!broadcast) {
|
|
177
|
+
await Promise.all(votesPromises);
|
|
178
|
+
// Still submit votes even without a checkpoint
|
|
179
|
+
if (!this.config.fishermanMode) {
|
|
180
|
+
this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(() => {});
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
122
183
|
}
|
|
123
184
|
|
|
185
|
+
const { checkpoint } = broadcast;
|
|
186
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
187
|
+
|
|
124
188
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
125
189
|
if (this.config.fishermanMode) {
|
|
126
190
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
127
|
-
return;
|
|
191
|
+
return checkpoint;
|
|
128
192
|
}
|
|
129
193
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
194
|
+
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
195
|
+
this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
|
|
196
|
+
|
|
197
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
198
|
+
return checkpoint;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
203
|
+
* Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
|
|
204
|
+
*/
|
|
205
|
+
private async waitForAttestationsAndEnqueueSubmissionAsync(
|
|
206
|
+
broadcast: CheckpointProposalBroadcast,
|
|
207
|
+
votesPromises: Promise<unknown>[],
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
const { checkpoint, proposal, blockProposedAt } = broadcast;
|
|
210
|
+
try {
|
|
211
|
+
await Promise.all(votesPromises);
|
|
212
|
+
|
|
213
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
214
|
+
const attestations = await this.waitForAttestations(proposal);
|
|
215
|
+
|
|
216
|
+
this.metrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
|
|
217
|
+
|
|
218
|
+
// Proposer must sign over the attestations before pushing them to L1
|
|
219
|
+
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
220
|
+
let attestationsSignature: Signature;
|
|
221
|
+
try {
|
|
222
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
223
|
+
attestations,
|
|
224
|
+
signer,
|
|
225
|
+
this.targetSlot,
|
|
226
|
+
this.checkpointNumber,
|
|
227
|
+
);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Enqueue the checkpoint for L1 submission
|
|
236
|
+
await this.enqueueCheckpointForSubmission({ checkpoint, attestations, attestationsSignature });
|
|
237
|
+
|
|
238
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
239
|
+
const submitAfter = this.epochCache.isProposerPipeliningEnabled()
|
|
240
|
+
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
241
|
+
: new Date(this.dateProvider.now());
|
|
242
|
+
|
|
243
|
+
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
244
|
+
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
245
|
+
if (proposedAction) {
|
|
246
|
+
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
247
|
+
const coinbase = checkpoint.header.coinbase;
|
|
248
|
+
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
249
|
+
} else {
|
|
250
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
251
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
252
|
+
this.metrics.recordPipelineDiscard();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err instanceof SequencerInterruptedError) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
|
|
260
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
261
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
262
|
+
this.metrics.recordPipelineDiscard();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
|
|
268
|
+
private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
|
|
269
|
+
const { checkpoint, attestations, attestationsSignature } = result;
|
|
270
|
+
|
|
271
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
272
|
+
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
273
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
274
|
+
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
275
|
+
|
|
276
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
277
|
+
if (
|
|
278
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
279
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
280
|
+
) {
|
|
281
|
+
const roll = Math.max(0, randomInt(100));
|
|
282
|
+
if (roll < this.config.skipPublishingCheckpointsPercent) {
|
|
283
|
+
this.log.warn(
|
|
284
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`,
|
|
285
|
+
);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
141
288
|
}
|
|
289
|
+
|
|
290
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
291
|
+
const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
|
|
292
|
+
pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
|
|
293
|
+
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
294
|
+
lastArchiveRoot: checkpoint.header.lastArchiveRoot,
|
|
295
|
+
pipeliningEnabled: isPipelining,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
299
|
+
txTimeoutAt,
|
|
300
|
+
...(submissionSimulationOverridesPlan ? { simulationOverridesPlan: submissionSimulationOverridesPlan } : {}),
|
|
301
|
+
});
|
|
142
302
|
}
|
|
143
303
|
|
|
144
304
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
145
305
|
return {
|
|
146
306
|
// nullish operator needed for tests
|
|
147
307
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
148
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
308
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
149
309
|
};
|
|
150
310
|
})
|
|
151
|
-
private async proposeCheckpoint(): Promise<
|
|
311
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
|
|
152
312
|
try {
|
|
153
313
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
154
314
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
155
315
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
156
316
|
|
|
157
317
|
// Start the checkpoint
|
|
158
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
159
|
-
this.
|
|
318
|
+
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
319
|
+
this.log.info(`Starting checkpoint proposal`, {
|
|
320
|
+
buildSlot: this.slotNow,
|
|
321
|
+
submissionSlot: this.targetSlot,
|
|
322
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
323
|
+
proposer: this.proposer?.toString(),
|
|
324
|
+
coinbase: coinbase.toString(),
|
|
325
|
+
});
|
|
326
|
+
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
160
327
|
|
|
161
328
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
162
329
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
163
330
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
164
331
|
}
|
|
165
332
|
|
|
166
|
-
// Create checkpoint builder for the slot
|
|
333
|
+
// Create checkpoint builder for the slot.
|
|
334
|
+
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
335
|
+
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
336
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
337
|
+
this.pipelinedParentSimulationOverridesPlan = isPipelining
|
|
338
|
+
? await buildPipelinedParentSimulationOverridesPlan({
|
|
339
|
+
checkpointNumber: this.checkpointNumber,
|
|
340
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
341
|
+
rollup: this.publisher.rollupContract,
|
|
342
|
+
log: this.log,
|
|
343
|
+
})
|
|
344
|
+
: undefined;
|
|
345
|
+
|
|
167
346
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
168
347
|
coinbase,
|
|
169
348
|
feeRecipient,
|
|
170
|
-
this.
|
|
349
|
+
this.targetSlot,
|
|
350
|
+
this.pipelinedParentSimulationOverridesPlan,
|
|
171
351
|
);
|
|
172
352
|
|
|
173
353
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
@@ -175,21 +355,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
175
355
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
176
356
|
|
|
177
357
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
178
|
-
const
|
|
179
|
-
c => c.
|
|
180
|
-
|
|
181
|
-
|
|
358
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch))
|
|
359
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
360
|
+
.map(c => c.checkpointOutHash);
|
|
361
|
+
|
|
362
|
+
// Get the fee asset price modifier from the oracle
|
|
363
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
182
364
|
|
|
183
365
|
// Create a long-lived forked world state for the checkpoint builder
|
|
184
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
366
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
185
367
|
|
|
186
368
|
// Create checkpoint builder for the entire slot
|
|
187
369
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
188
370
|
this.checkpointNumber,
|
|
189
371
|
checkpointGlobalVariables,
|
|
372
|
+
feeAssetPriceModifier,
|
|
190
373
|
l1ToL2Messages,
|
|
191
374
|
previousCheckpointOutHashes,
|
|
192
375
|
fork,
|
|
376
|
+
this.log.getBindings(),
|
|
193
377
|
);
|
|
194
378
|
|
|
195
379
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -204,7 +388,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
204
388
|
};
|
|
205
389
|
|
|
206
390
|
let blocksInCheckpoint: L2Block[] = [];
|
|
207
|
-
let blockPendingBroadcast:
|
|
391
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
392
|
+
const checkpointBuildTimer = new Timer();
|
|
208
393
|
|
|
209
394
|
try {
|
|
210
395
|
// Main loop: build blocks for the checkpoint
|
|
@@ -220,62 +405,79 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
220
405
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
221
406
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
222
407
|
// 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
|
-
});
|
|
408
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
236
409
|
return undefined;
|
|
237
410
|
}
|
|
238
411
|
throw err;
|
|
239
412
|
}
|
|
240
413
|
|
|
241
414
|
if (blocksInCheckpoint.length === 0) {
|
|
242
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
243
|
-
this.eventEmitter.emit('checkpoint-empty', { slot: this.
|
|
415
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, { slot: this.targetSlot });
|
|
416
|
+
this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
421
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
422
|
+
this.log.warn(
|
|
423
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
424
|
+
{ slot: this.targetSlot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
425
|
+
);
|
|
244
426
|
return undefined;
|
|
245
427
|
}
|
|
246
428
|
|
|
247
429
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
248
430
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
249
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
431
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
250
432
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
251
433
|
|
|
252
|
-
//
|
|
434
|
+
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
435
|
+
// Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
|
|
436
|
+
try {
|
|
437
|
+
validateCheckpoint(checkpoint, {
|
|
438
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
439
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
440
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
441
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
442
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
443
|
+
});
|
|
444
|
+
} catch (err) {
|
|
445
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
446
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
447
|
+
});
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Record checkpoint-level build metrics
|
|
452
|
+
this.metrics.recordCheckpointBuild(
|
|
453
|
+
checkpointBuildTimer.ms(),
|
|
454
|
+
blocksInCheckpoint.length,
|
|
455
|
+
checkpoint.getStats().txCount,
|
|
456
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
253
460
|
if (this.config.fishermanMode) {
|
|
254
461
|
this.log.info(
|
|
255
|
-
`Built checkpoint for slot ${this.
|
|
462
|
+
`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
|
|
256
463
|
`Skipping proposal in fisherman mode.`,
|
|
257
464
|
{
|
|
258
|
-
slot: this.
|
|
465
|
+
slot: this.targetSlot,
|
|
259
466
|
checkpoint: checkpoint.header.toInspect(),
|
|
260
467
|
blocksBuilt: blocksInCheckpoint.length,
|
|
261
468
|
},
|
|
262
469
|
);
|
|
263
470
|
this.metrics.recordCheckpointSuccess();
|
|
264
|
-
|
|
471
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
472
|
+
return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
|
|
265
473
|
}
|
|
266
474
|
|
|
267
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
268
|
-
const lastBlock = blockPendingBroadcast && {
|
|
269
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
270
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
271
|
-
txs: blockPendingBroadcast.txs,
|
|
272
|
-
};
|
|
273
|
-
|
|
274
475
|
// Create the checkpoint proposal and broadcast it
|
|
275
476
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
477
|
checkpoint.header,
|
|
277
478
|
checkpoint.archive.root,
|
|
278
|
-
|
|
479
|
+
feeAssetPriceModifier,
|
|
480
|
+
blockPendingBroadcast,
|
|
279
481
|
this.proposer,
|
|
280
482
|
checkpointProposalOptions,
|
|
281
483
|
);
|
|
@@ -283,61 +485,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
283
485
|
const blockProposedAt = this.dateProvider.now();
|
|
284
486
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
285
487
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const blockAttestedAt = this.dateProvider.now();
|
|
289
|
-
|
|
290
|
-
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
291
|
-
|
|
292
|
-
// Proposer must sign over the attestations before pushing them to L1
|
|
293
|
-
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
294
|
-
let attestationsSignature: Signature;
|
|
295
|
-
try {
|
|
296
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
297
|
-
attestations,
|
|
298
|
-
signer,
|
|
299
|
-
this.slot,
|
|
300
|
-
this.checkpointNumber,
|
|
301
|
-
);
|
|
302
|
-
} catch (err) {
|
|
303
|
-
// 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 instanceof DutyAlreadySignedError) {
|
|
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
|
-
});
|
|
318
|
-
return undefined;
|
|
319
|
-
}
|
|
320
|
-
throw err;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Enqueue publishing the checkpoint to L1
|
|
324
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
325
|
-
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
326
|
-
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
327
|
-
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
328
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
329
|
-
txTimeoutAt,
|
|
330
|
-
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
return checkpoint;
|
|
488
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
489
|
+
return { checkpoint, proposal, blockProposedAt };
|
|
334
490
|
} catch (err) {
|
|
335
491
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
336
492
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
337
493
|
return undefined;
|
|
338
494
|
}
|
|
339
495
|
|
|
340
|
-
this.log.error(`Error building checkpoint at slot ${this.
|
|
496
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
341
497
|
return undefined;
|
|
342
498
|
}
|
|
343
499
|
}
|
|
@@ -353,21 +509,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
353
509
|
blockProposalOptions: BlockProposalOptions,
|
|
354
510
|
): Promise<{
|
|
355
511
|
blocksInCheckpoint: L2Block[];
|
|
356
|
-
blockPendingBroadcast:
|
|
512
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
357
513
|
}> {
|
|
358
514
|
const blocksInCheckpoint: L2Block[] = [];
|
|
359
515
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
360
516
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
361
517
|
|
|
362
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
363
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
364
|
-
|
|
365
518
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
366
|
-
let blockPendingBroadcast:
|
|
519
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
367
520
|
|
|
368
521
|
while (true) {
|
|
369
522
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
370
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
523
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
371
524
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
372
525
|
|
|
373
526
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
@@ -375,7 +528,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
375
528
|
|
|
376
529
|
if (!timingInfo.canStart) {
|
|
377
530
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
378
|
-
slot: this.
|
|
531
|
+
slot: this.targetSlot,
|
|
379
532
|
blocksBuilt,
|
|
380
533
|
secondsIntoSlot,
|
|
381
534
|
});
|
|
@@ -394,24 +547,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
394
547
|
blockNumber,
|
|
395
548
|
indexWithinCheckpoint,
|
|
396
549
|
txHashesAlreadyIncluded,
|
|
397
|
-
remainingBlobFields,
|
|
398
550
|
});
|
|
399
551
|
|
|
400
|
-
if
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
552
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
553
|
+
if ('failure' in buildResult) {
|
|
554
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
555
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
405
559
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
406
560
|
continue;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
564
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
565
|
+
if ('error' in buildResult) {
|
|
412
566
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
413
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
414
|
-
slot: this.
|
|
567
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
568
|
+
slot: this.targetSlot,
|
|
415
569
|
blocksBuilt,
|
|
416
570
|
error: buildResult.error,
|
|
417
571
|
});
|
|
@@ -419,210 +573,256 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
419
573
|
break;
|
|
420
574
|
}
|
|
421
575
|
|
|
422
|
-
const { block, usedTxs
|
|
576
|
+
const { block, usedTxs } = buildResult;
|
|
423
577
|
blocksInCheckpoint.push(block);
|
|
578
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
424
579
|
|
|
425
|
-
//
|
|
426
|
-
|
|
580
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
581
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
427
582
|
|
|
428
|
-
// Sync the proposed block to the archiver to make it available
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
this.syncProposedBlockToArchiver(block)
|
|
433
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
434
|
-
});
|
|
583
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
584
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
585
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
586
|
+
// If this throws, we abort the entire checkpoint.
|
|
587
|
+
await this.syncProposedBlockToArchiver(block);
|
|
435
588
|
|
|
436
|
-
// If this is the last block,
|
|
589
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
437
590
|
if (timingInfo.isLastBlock) {
|
|
438
|
-
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.
|
|
439
|
-
slot: this.
|
|
591
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
592
|
+
slot: this.targetSlot,
|
|
440
593
|
blockNumber,
|
|
441
594
|
blocksBuilt,
|
|
442
595
|
});
|
|
443
|
-
|
|
596
|
+
|
|
597
|
+
blockPendingBroadcast = proposal;
|
|
444
598
|
break;
|
|
445
599
|
}
|
|
446
600
|
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
if (!this.config.fishermanMode) {
|
|
450
|
-
const proposal = await this.validatorClient.createBlockProposal(
|
|
451
|
-
block.header,
|
|
452
|
-
block.indexWithinCheckpoint,
|
|
453
|
-
inHash,
|
|
454
|
-
block.archive.root,
|
|
455
|
-
usedTxs,
|
|
456
|
-
this.proposer,
|
|
457
|
-
blockProposalOptions,
|
|
458
|
-
);
|
|
459
|
-
await this.p2pClient.broadcastProposal(proposal);
|
|
460
|
-
}
|
|
601
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
602
|
+
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
461
603
|
|
|
462
604
|
// Wait until the next block's start time
|
|
463
605
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
464
606
|
}
|
|
465
607
|
|
|
466
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
467
|
-
slot: this.
|
|
608
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
609
|
+
slot: this.targetSlot,
|
|
468
610
|
blocksBuilt: blocksInCheckpoint.length,
|
|
469
611
|
});
|
|
470
612
|
|
|
471
613
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
472
614
|
}
|
|
473
615
|
|
|
616
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
|
|
617
|
+
private createBlockProposal(
|
|
618
|
+
block: L2Block,
|
|
619
|
+
inHash: Fr,
|
|
620
|
+
usedTxs: Tx[],
|
|
621
|
+
blockProposalOptions: BlockProposalOptions,
|
|
622
|
+
): Promise<BlockProposal | undefined> {
|
|
623
|
+
if (this.config.fishermanMode) {
|
|
624
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
625
|
+
return Promise.resolve(undefined);
|
|
626
|
+
}
|
|
627
|
+
return this.validatorClient.createBlockProposal(
|
|
628
|
+
block.header,
|
|
629
|
+
block.indexWithinCheckpoint,
|
|
630
|
+
inHash,
|
|
631
|
+
block.archive.root,
|
|
632
|
+
usedTxs,
|
|
633
|
+
this.proposer,
|
|
634
|
+
blockProposalOptions,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
474
638
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
475
639
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
476
640
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
477
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.
|
|
478
|
-
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
641
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
642
|
+
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
643
|
+
slot: this.targetSlot,
|
|
644
|
+
});
|
|
479
645
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
480
646
|
}
|
|
481
647
|
|
|
482
648
|
/** Builds a single block. Called from the main block building loop. */
|
|
483
649
|
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
484
|
-
|
|
650
|
+
protected async buildSingleBlock(
|
|
485
651
|
checkpointBuilder: CheckpointBuilder,
|
|
486
652
|
opts: {
|
|
487
653
|
forceCreate?: boolean;
|
|
488
654
|
blockTimestamp: bigint;
|
|
489
655
|
blockNumber: BlockNumber;
|
|
490
|
-
indexWithinCheckpoint:
|
|
656
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
491
657
|
buildDeadline: Date | undefined;
|
|
492
658
|
txHashesAlreadyIncluded: Set<string>;
|
|
493
|
-
remainingBlobFields: number;
|
|
494
659
|
},
|
|
495
|
-
): Promise<
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
indexWithinCheckpoint,
|
|
501
|
-
buildDeadline,
|
|
502
|
-
txHashesAlreadyIncluded,
|
|
503
|
-
remainingBlobFields,
|
|
504
|
-
} = opts;
|
|
660
|
+
): Promise<
|
|
661
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
662
|
+
> {
|
|
663
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
664
|
+
opts;
|
|
505
665
|
|
|
506
666
|
this.log.verbose(
|
|
507
|
-
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
667
|
+
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`,
|
|
508
668
|
{ ...checkpointBuilder.getConstantData(), ...opts },
|
|
509
669
|
);
|
|
510
670
|
|
|
511
671
|
try {
|
|
512
672
|
// Wait until we have enough txs to build the block
|
|
513
|
-
const minTxs = this.
|
|
514
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
673
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
515
674
|
if (!canStartBuilding) {
|
|
516
675
|
this.log.warn(
|
|
517
|
-
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
518
|
-
{ blockNumber, slot: this.
|
|
676
|
+
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
677
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
519
678
|
);
|
|
520
|
-
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.
|
|
679
|
+
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
521
680
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
522
|
-
return
|
|
681
|
+
return { failure: 'insufficient-txs' };
|
|
523
682
|
}
|
|
524
683
|
|
|
525
684
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
526
685
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
527
686
|
const pendingTxs = filter(
|
|
528
|
-
this.p2pClient.
|
|
687
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
529
688
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
530
689
|
);
|
|
531
690
|
|
|
532
691
|
this.log.debug(
|
|
533
|
-
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
534
|
-
{ slot: this.
|
|
692
|
+
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
|
|
693
|
+
{ slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
|
|
535
694
|
);
|
|
536
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
537
|
-
|
|
538
|
-
// Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
|
|
539
|
-
const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
|
|
540
|
-
const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
|
|
695
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
541
696
|
|
|
542
|
-
|
|
697
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
698
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
699
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
700
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
701
|
+
const blockBuilderOptions: BlockBuilderOptions = {
|
|
543
702
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
703
|
+
maxBlockGas:
|
|
704
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
705
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
706
|
+
: undefined,
|
|
547
707
|
deadline: buildDeadline,
|
|
708
|
+
isBuildingProposal: true,
|
|
709
|
+
minValidTxs,
|
|
710
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
711
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
548
712
|
};
|
|
549
713
|
|
|
550
|
-
// Actually build the block by executing txs
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
usedTxBlobFields,
|
|
561
|
-
} = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
562
|
-
const blockBuildDuration = workTimer.ms();
|
|
714
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
715
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
716
|
+
// updated for blocks that will be discarded.
|
|
717
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
718
|
+
checkpointBuilder,
|
|
719
|
+
pendingTxs,
|
|
720
|
+
blockNumber,
|
|
721
|
+
blockTimestamp,
|
|
722
|
+
blockBuilderOptions,
|
|
723
|
+
);
|
|
563
724
|
|
|
564
725
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
565
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
726
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
566
727
|
|
|
567
|
-
|
|
568
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
569
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
570
|
-
if (!forceCreate && numTxs < minValidTxs) {
|
|
728
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
571
729
|
this.log.warn(
|
|
572
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.
|
|
573
|
-
{
|
|
730
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
|
|
731
|
+
{
|
|
732
|
+
slot: this.targetSlot,
|
|
733
|
+
blockNumber,
|
|
734
|
+
numTxs: buildResult.processedCount,
|
|
735
|
+
indexWithinCheckpoint,
|
|
736
|
+
minValidTxs,
|
|
737
|
+
},
|
|
574
738
|
);
|
|
575
|
-
this.eventEmitter.emit('block-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
slot: this.slot,
|
|
739
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
740
|
+
reason: `Insufficient valid txs`,
|
|
741
|
+
slot: this.targetSlot,
|
|
579
742
|
});
|
|
580
743
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
581
|
-
return
|
|
744
|
+
return { failure: 'insufficient-valid-txs' };
|
|
582
745
|
}
|
|
583
746
|
|
|
584
747
|
// Block creation succeeded, emit stats and metrics
|
|
748
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
749
|
+
|
|
585
750
|
const blockStats = {
|
|
586
751
|
eventName: 'l2-block-built',
|
|
587
752
|
duration: blockBuildDuration,
|
|
588
753
|
publicProcessDuration: publicProcessorDuration,
|
|
589
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
590
754
|
...block.getStats(),
|
|
591
755
|
} satisfies L2BlockBuiltStats;
|
|
592
756
|
|
|
593
757
|
const blockHash = await block.hash();
|
|
594
758
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
595
|
-
const manaPerSec =
|
|
759
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
596
760
|
|
|
597
761
|
this.log.info(
|
|
598
|
-
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.
|
|
762
|
+
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`,
|
|
599
763
|
{ blockHash, txHashes, manaPerSec, ...blockStats },
|
|
600
764
|
);
|
|
601
765
|
|
|
602
|
-
|
|
603
|
-
|
|
766
|
+
// `slot` is the target/submission slot (may be one ahead when pipelining),
|
|
767
|
+
// `buildSlot` is the wall-clock slot during which the block was actually built.
|
|
768
|
+
this.eventEmitter.emit('block-proposed', {
|
|
769
|
+
blockNumber: block.number,
|
|
770
|
+
slot: this.targetSlot,
|
|
771
|
+
buildSlot: this.slotNow,
|
|
772
|
+
});
|
|
773
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
604
774
|
|
|
605
|
-
return { block, usedTxs
|
|
775
|
+
return { block, usedTxs };
|
|
606
776
|
} catch (err: any) {
|
|
607
|
-
this.eventEmitter.emit('block-build-failed', {
|
|
608
|
-
|
|
777
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
778
|
+
reason: err.message,
|
|
779
|
+
slot: this.targetSlot,
|
|
780
|
+
});
|
|
781
|
+
this.log.error(`Error building block`, err, { blockNumber, slot: this.targetSlot });
|
|
609
782
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
610
783
|
this.metrics.recordFailedBlock();
|
|
611
784
|
return { error: err };
|
|
612
785
|
}
|
|
613
786
|
}
|
|
614
787
|
|
|
788
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
789
|
+
private async buildSingleBlockWithCheckpointBuilder(
|
|
790
|
+
checkpointBuilder: CheckpointBuilder,
|
|
791
|
+
pendingTxs: AsyncIterable<Tx>,
|
|
792
|
+
blockNumber: BlockNumber,
|
|
793
|
+
blockTimestamp: bigint,
|
|
794
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
795
|
+
) {
|
|
796
|
+
try {
|
|
797
|
+
const workTimer = new Timer();
|
|
798
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
799
|
+
const blockBuildDuration = workTimer.ms();
|
|
800
|
+
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
801
|
+
} catch (err: unknown) {
|
|
802
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
803
|
+
return {
|
|
804
|
+
failedTxs: err.failedTxs,
|
|
805
|
+
processedCount: err.processedCount,
|
|
806
|
+
status: 'insufficient-valid-txs' as const,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
throw err;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
615
813
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
616
814
|
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
617
815
|
private async waitForMinTxs(opts: {
|
|
618
816
|
forceCreate?: boolean;
|
|
619
817
|
blockNumber: BlockNumber;
|
|
620
|
-
indexWithinCheckpoint:
|
|
818
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
621
819
|
buildDeadline: Date | undefined;
|
|
622
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
623
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
820
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
624
821
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
625
822
|
|
|
823
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
824
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
825
|
+
|
|
626
826
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
627
827
|
const startBuildingDeadline = buildDeadline
|
|
628
828
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -634,20 +834,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
634
834
|
// If we're past deadline, or we have no deadline, give up
|
|
635
835
|
const now = this.dateProvider.nowAsDate();
|
|
636
836
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
637
|
-
return { canStartBuilding: false, availableTxs
|
|
837
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
638
838
|
}
|
|
639
839
|
|
|
640
840
|
// Wait a bit before checking again
|
|
641
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
841
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
642
842
|
this.log.verbose(
|
|
643
|
-
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
644
|
-
{ blockNumber, slot: this.
|
|
843
|
+
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
|
|
844
|
+
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
645
845
|
);
|
|
646
|
-
await
|
|
846
|
+
await this.waitForTxsPollingInterval();
|
|
647
847
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
648
848
|
}
|
|
649
849
|
|
|
650
|
-
return { canStartBuilding: true, availableTxs };
|
|
850
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
651
851
|
}
|
|
652
852
|
|
|
653
853
|
/**
|
|
@@ -673,7 +873,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
673
873
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
674
874
|
}
|
|
675
875
|
|
|
676
|
-
const numberOfRequiredAttestations =
|
|
876
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
677
877
|
|
|
678
878
|
if (this.config.skipCollectingAttestations) {
|
|
679
879
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
@@ -682,9 +882,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
682
882
|
}
|
|
683
883
|
|
|
684
884
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
685
|
-
? this.timetable.
|
|
885
|
+
? this.timetable.getCheckpointAttestationDeadline()
|
|
686
886
|
: this.l1Constants.slotDuration;
|
|
687
|
-
const attestationDeadline = new Date(this.
|
|
887
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
688
888
|
|
|
689
889
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
690
890
|
|
|
@@ -699,11 +899,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
699
899
|
|
|
700
900
|
collectedAttestationsCount = attestations.length;
|
|
701
901
|
|
|
902
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
903
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
904
|
+
const trimmed = trimAttestations(
|
|
905
|
+
attestations,
|
|
906
|
+
numberOfRequiredAttestations,
|
|
907
|
+
this.attestorAddress,
|
|
908
|
+
localAddresses,
|
|
909
|
+
);
|
|
910
|
+
if (trimmed.length < attestations.length) {
|
|
911
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
912
|
+
}
|
|
913
|
+
|
|
702
914
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
703
|
-
const sorted = orderAttestations(
|
|
915
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
704
916
|
|
|
705
917
|
// Manipulate the attestations if we've been configured to do so
|
|
706
|
-
if (
|
|
918
|
+
if (
|
|
919
|
+
this.config.injectFakeAttestation ||
|
|
920
|
+
this.config.injectHighSValueAttestation ||
|
|
921
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
922
|
+
this.config.shuffleAttestationOrdering
|
|
923
|
+
) {
|
|
707
924
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
708
925
|
}
|
|
709
926
|
|
|
@@ -732,7 +949,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
732
949
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
733
950
|
);
|
|
734
951
|
|
|
735
|
-
if (
|
|
952
|
+
if (
|
|
953
|
+
this.config.injectFakeAttestation ||
|
|
954
|
+
this.config.injectHighSValueAttestation ||
|
|
955
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
956
|
+
) {
|
|
736
957
|
// Find non-empty attestations that are not from the proposer
|
|
737
958
|
const nonProposerIndices: number[] = [];
|
|
738
959
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -742,8 +963,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
742
963
|
}
|
|
743
964
|
if (nonProposerIndices.length > 0) {
|
|
744
965
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
745
|
-
this.
|
|
746
|
-
|
|
966
|
+
if (this.config.injectHighSValueAttestation) {
|
|
967
|
+
this.log.warn(
|
|
968
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
969
|
+
);
|
|
970
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
971
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
972
|
+
this.log.warn(
|
|
973
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
974
|
+
);
|
|
975
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
976
|
+
} else {
|
|
977
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
978
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
979
|
+
}
|
|
747
980
|
}
|
|
748
981
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
749
982
|
}
|
|
@@ -752,11 +985,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
752
985
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
753
986
|
|
|
754
987
|
const shuffled = [...attestations];
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
988
|
+
|
|
989
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
990
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
991
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
992
|
+
const swappable: number[] = [];
|
|
993
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
994
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
995
|
+
swappable.push(k);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (swappable.length >= 2) {
|
|
999
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
1000
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
1001
|
+
}
|
|
760
1002
|
|
|
761
1003
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
762
1004
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -772,16 +1014,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
772
1014
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
773
1015
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
774
1016
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
775
|
-
await this.p2pClient.
|
|
1017
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
776
1018
|
}
|
|
777
1019
|
|
|
778
1020
|
/**
|
|
779
1021
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
780
1022
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
781
1023
|
* would never receive its own block without this explicit sync.
|
|
1024
|
+
*
|
|
1025
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1026
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1027
|
+
* whenever the real proposer's block arrives from L1.
|
|
782
1028
|
*/
|
|
783
1029
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
784
|
-
if (this.config.skipPushProposedBlocksToArchiver
|
|
1030
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
785
1031
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
786
1032
|
blockNumber: block.number,
|
|
787
1033
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -799,27 +1045,49 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
799
1045
|
private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
|
|
800
1046
|
// Perform L1 fee analysis before clearing requests
|
|
801
1047
|
// The callback is invoked asynchronously after the next block is mined
|
|
802
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1048
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, analysis =>
|
|
803
1049
|
this.metrics.recordFishermanFeeAnalysis(analysis),
|
|
804
1050
|
);
|
|
805
1051
|
|
|
806
1052
|
if (checkpoint) {
|
|
807
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1053
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
808
1054
|
...checkpoint.toCheckpointInfo(),
|
|
809
1055
|
...checkpoint.getStats(),
|
|
810
1056
|
feeAnalysisId: feeAnalysis?.id,
|
|
811
1057
|
});
|
|
812
1058
|
} else {
|
|
813
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
814
|
-
slot: this.
|
|
1059
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1060
|
+
slot: this.targetSlot,
|
|
815
1061
|
feeAnalysisId: feeAnalysis?.id,
|
|
816
1062
|
});
|
|
817
|
-
this.metrics.
|
|
1063
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
818
1064
|
}
|
|
819
1065
|
|
|
820
1066
|
this.publisher.clearPendingRequests();
|
|
821
1067
|
}
|
|
822
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
1071
|
+
*/
|
|
1072
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
1073
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
1074
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
|
|
1075
|
+
slot: this.targetSlot,
|
|
1076
|
+
signedByNode: err.signedByNode,
|
|
1077
|
+
});
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
if (err instanceof SlashingProtectionError) {
|
|
1081
|
+
this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
|
|
1082
|
+
slot: this.targetSlot,
|
|
1083
|
+
existingMessageHash: err.existingMessageHash,
|
|
1084
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
1085
|
+
});
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
823
1091
|
/** Waits until a specific time within the current slot */
|
|
824
1092
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
825
1093
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -828,8 +1096,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
828
1096
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
829
1097
|
}
|
|
830
1098
|
|
|
1099
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
1100
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
1101
|
+
await sleep(TXS_POLLING_MS);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
831
1104
|
private getSlotStartBuildTimestamp(): number {
|
|
832
|
-
return getSlotStartBuildTimestamp(this.
|
|
1105
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
833
1106
|
}
|
|
834
1107
|
|
|
835
1108
|
private getSecondsIntoSlot(): number {
|