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