@aztec/sequencer-client 0.0.1-commit.1142ef1 → 0.0.1-commit.11bf3dd6e
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/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +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 +54 -12
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +450 -184
- 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 +3 -3
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -13
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -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 +58 -22
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +218 -96
- 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 +23 -19
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +67 -38
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +12 -11
- package/package.json +29 -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/index.ts +1 -6
- 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 +601 -206
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +31 -17
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +136 -40
- package/src/sequencer/sequencer.ts +302 -105
- 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 +122 -78
- package/src/test/utils.ts +24 -14
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -1,43 +1,67 @@
|
|
|
1
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
2
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
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';
|
|
4
10
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
11
|
+
import {
|
|
12
|
+
flipSignature,
|
|
13
|
+
generateRecoverableSignature,
|
|
14
|
+
generateUnrecoverableSignature,
|
|
15
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
5
16
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
17
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
18
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
19
|
import { filter } from '@aztec/foundation/iterator';
|
|
9
|
-
import type
|
|
20
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
10
21
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
11
22
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
12
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
23
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
13
24
|
import type { P2P } from '@aztec/p2p';
|
|
14
25
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
26
|
import {
|
|
16
27
|
CommitteeAttestation,
|
|
17
28
|
CommitteeAttestationsAndSigners,
|
|
18
|
-
|
|
29
|
+
L2Block,
|
|
19
30
|
type L2BlockSink,
|
|
31
|
+
type L2BlockSource,
|
|
20
32
|
MaliciousCommitteeAttestationsAndSigners,
|
|
21
33
|
} from '@aztec/stdlib/block';
|
|
22
|
-
import type
|
|
23
|
-
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';
|
|
24
36
|
import { Gas } from '@aztec/stdlib/gas';
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
import {
|
|
38
|
+
type BlockBuilderOptions,
|
|
39
|
+
InsufficientValidTxsError,
|
|
40
|
+
type ResolvedSequencerConfig,
|
|
41
|
+
type WorldStateSynchronizer,
|
|
29
42
|
} from '@aztec/stdlib/interfaces/server';
|
|
30
43
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
31
|
-
import type {
|
|
32
|
-
|
|
44
|
+
import type {
|
|
45
|
+
BlockProposal,
|
|
46
|
+
BlockProposalOptions,
|
|
47
|
+
CheckpointProposal,
|
|
48
|
+
CheckpointProposalOptions,
|
|
49
|
+
} from '@aztec/stdlib/p2p';
|
|
50
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
33
51
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
34
52
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
35
53
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
36
54
|
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
37
55
|
import { CheckpointBuilder, type FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
|
|
56
|
+
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
38
57
|
|
|
39
58
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
40
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';
|
|
41
65
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
42
66
|
import { SequencerInterruptedError } from './errors.js';
|
|
43
67
|
import type { SequencerEvents } from './events.js';
|
|
@@ -49,6 +73,20 @@ import { SequencerState } from './utils.js';
|
|
|
49
73
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
50
74
|
const TXS_POLLING_MS = 500;
|
|
51
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
|
+
|
|
52
90
|
/**
|
|
53
91
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
54
92
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -56,8 +94,18 @@ const TXS_POLLING_MS = 500;
|
|
|
56
94
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
57
95
|
*/
|
|
58
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
|
+
|
|
59
105
|
constructor(
|
|
60
|
-
private readonly
|
|
106
|
+
private readonly slotNow: SlotNumber,
|
|
107
|
+
private readonly targetSlot: SlotNumber,
|
|
108
|
+
private readonly targetEpoch: EpochNumber,
|
|
61
109
|
private readonly checkpointNumber: CheckpointNumber,
|
|
62
110
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
63
111
|
// TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
@@ -70,6 +118,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
70
118
|
private readonly p2pClient: P2P,
|
|
71
119
|
private readonly worldState: WorldStateSynchronizer,
|
|
72
120
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
121
|
+
private readonly l2BlockSource: L2BlockSource,
|
|
73
122
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
74
123
|
private readonly blockSink: L2BlockSink,
|
|
75
124
|
private readonly l1Constants: SequencerRollupConstants,
|
|
@@ -79,15 +128,31 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
79
128
|
private readonly epochCache: EpochCache,
|
|
80
129
|
private readonly dateProvider: DateProvider,
|
|
81
130
|
private readonly metrics: SequencerMetrics,
|
|
131
|
+
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
82
132
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
83
133
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
84
|
-
protected readonly log: Logger,
|
|
85
134
|
public readonly tracer: Tracer,
|
|
86
|
-
|
|
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
|
+
}
|
|
87
149
|
|
|
88
150
|
/**
|
|
89
151
|
* Executes the checkpoint proposal job.
|
|
90
|
-
*
|
|
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.
|
|
91
156
|
*/
|
|
92
157
|
@trackSpan('CheckpointProposalJob.execute')
|
|
93
158
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
@@ -95,7 +160,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
95
160
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
96
161
|
// These are constant for the whole slot, so we only enqueue them once
|
|
97
162
|
const votesPromises = new CheckpointVoter(
|
|
98
|
-
this.
|
|
163
|
+
this.targetSlot,
|
|
99
164
|
this.publisher,
|
|
100
165
|
this.attestorAddress,
|
|
101
166
|
this.validatorClient,
|
|
@@ -106,78 +171,221 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
106
171
|
this.log,
|
|
107
172
|
).enqueueVotes();
|
|
108
173
|
|
|
109
|
-
// Build
|
|
110
|
-
|
|
174
|
+
// Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
|
|
175
|
+
// Returns after broadcast — attestation collection is deferred.
|
|
176
|
+
const broadcast = await this.proposeCheckpoint();
|
|
111
177
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
|
117
185
|
}
|
|
118
186
|
|
|
187
|
+
const { checkpoint } = broadcast;
|
|
188
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
189
|
+
|
|
119
190
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
120
191
|
if (this.config.fishermanMode) {
|
|
121
192
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
122
|
-
return;
|
|
193
|
+
return checkpoint;
|
|
123
194
|
}
|
|
124
195
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
266
|
}
|
|
137
267
|
}
|
|
138
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
|
+
|
|
139
306
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
140
307
|
return {
|
|
141
308
|
// nullish operator needed for tests
|
|
142
309
|
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
143
|
-
[Attributes.SLOT_NUMBER]: this.
|
|
310
|
+
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
144
311
|
};
|
|
145
312
|
})
|
|
146
|
-
private async proposeCheckpoint(): Promise<
|
|
313
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
|
|
147
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
|
+
|
|
148
325
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
149
326
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
150
327
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
151
328
|
|
|
152
329
|
// Start the checkpoint
|
|
153
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.
|
|
154
|
-
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');
|
|
155
339
|
|
|
156
340
|
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
157
341
|
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
158
342
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
159
343
|
}
|
|
160
344
|
|
|
161
|
-
// 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
|
+
|
|
162
358
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
163
359
|
coinbase,
|
|
164
360
|
feeRecipient,
|
|
165
|
-
this.
|
|
361
|
+
this.targetSlot,
|
|
362
|
+
this.pipelinedParentSimulationOverridesPlan,
|
|
166
363
|
);
|
|
167
364
|
|
|
168
365
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
169
366
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
170
367
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
171
368
|
|
|
369
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
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();
|
|
376
|
+
|
|
172
377
|
// Create a long-lived forked world state for the checkpoint builder
|
|
173
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
378
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
174
379
|
|
|
175
380
|
// Create checkpoint builder for the entire slot
|
|
176
381
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
177
382
|
this.checkpointNumber,
|
|
178
383
|
checkpointGlobalVariables,
|
|
384
|
+
feeAssetPriceModifier,
|
|
179
385
|
l1ToL2Messages,
|
|
386
|
+
previousCheckpointOutHashes,
|
|
180
387
|
fork,
|
|
388
|
+
this.log.getBindings(),
|
|
181
389
|
);
|
|
182
390
|
|
|
183
391
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -191,82 +399,115 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
191
399
|
broadcastInvalidCheckpointProposal: this.config.broadcastInvalidBlockProposal,
|
|
192
400
|
};
|
|
193
401
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
402
|
+
let blocksInCheckpoint: L2Block[] = [];
|
|
403
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
404
|
+
const checkpointBuildTimer = new Timer();
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
// Main loop: build blocks for the checkpoint
|
|
408
|
+
const result = await this.buildBlocksForCheckpoint(
|
|
409
|
+
checkpointBuilder,
|
|
410
|
+
checkpointGlobalVariables.timestamp,
|
|
411
|
+
inHash,
|
|
412
|
+
blockProposalOptions,
|
|
413
|
+
);
|
|
414
|
+
blocksInCheckpoint = result.blocksInCheckpoint;
|
|
415
|
+
blockPendingBroadcast = result.blockPendingBroadcast;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
418
|
+
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
419
|
+
// which is normal for block building (may have picked different txs)
|
|
420
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
201
425
|
|
|
202
426
|
if (blocksInCheckpoint.length === 0) {
|
|
203
|
-
this.log.warn(`No blocks were built for slot ${this.
|
|
204
|
-
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
|
+
);
|
|
205
438
|
return undefined;
|
|
206
439
|
}
|
|
207
440
|
|
|
208
441
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
209
442
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
210
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.
|
|
443
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
211
444
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
212
445
|
|
|
213
|
-
//
|
|
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
|
|
214
472
|
if (this.config.fishermanMode) {
|
|
215
473
|
this.log.info(
|
|
216
|
-
`Built checkpoint for slot ${this.
|
|
474
|
+
`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
|
|
217
475
|
`Skipping proposal in fisherman mode.`,
|
|
218
476
|
{
|
|
219
|
-
slot: this.
|
|
477
|
+
slot: this.targetSlot,
|
|
220
478
|
checkpoint: checkpoint.header.toInspect(),
|
|
221
479
|
blocksBuilt: blocksInCheckpoint.length,
|
|
222
480
|
},
|
|
223
481
|
);
|
|
224
482
|
this.metrics.recordCheckpointSuccess();
|
|
225
|
-
|
|
483
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
484
|
+
return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
|
|
226
485
|
}
|
|
227
486
|
|
|
228
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
229
|
-
const lastBlock = blockPendingBroadcast && {
|
|
230
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
231
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
232
|
-
txs: blockPendingBroadcast.txs,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
487
|
// Create the checkpoint proposal and broadcast it
|
|
236
488
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
237
489
|
checkpoint.header,
|
|
238
490
|
checkpoint.archive.root,
|
|
239
|
-
|
|
491
|
+
this.checkpointNumber,
|
|
492
|
+
feeAssetPriceModifier,
|
|
493
|
+
blockPendingBroadcast,
|
|
240
494
|
this.proposer,
|
|
241
495
|
checkpointProposalOptions,
|
|
242
496
|
);
|
|
243
497
|
|
|
244
498
|
const blockProposedAt = this.dateProvider.now();
|
|
245
499
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
500
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
246
501
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const blockAttestedAt = this.dateProvider.now();
|
|
250
|
-
|
|
251
|
-
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
252
|
-
|
|
253
|
-
// Proposer must sign over the attestations before pushing them to L1
|
|
254
|
-
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
255
|
-
const attestationsSignature = await this.validatorClient.signAttestationsAndSigners(attestations, signer);
|
|
256
|
-
|
|
257
|
-
// Enqueue publishing the checkpoint to L1
|
|
258
|
-
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
259
|
-
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
260
|
-
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
261
|
-
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
262
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
263
|
-
txTimeoutAt,
|
|
264
|
-
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
return checkpoint;
|
|
502
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
503
|
+
return { checkpoint, proposal, blockProposedAt };
|
|
268
504
|
} catch (err) {
|
|
269
|
-
|
|
505
|
+
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
506
|
+
// swallow this error. It's already been logged by a function deeper in the stack
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
270
511
|
return undefined;
|
|
271
512
|
}
|
|
272
513
|
}
|
|
@@ -281,19 +522,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
281
522
|
inHash: Fr,
|
|
282
523
|
blockProposalOptions: BlockProposalOptions,
|
|
283
524
|
): Promise<{
|
|
284
|
-
blocksInCheckpoint:
|
|
285
|
-
blockPendingBroadcast:
|
|
525
|
+
blocksInCheckpoint: L2Block[];
|
|
526
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
286
527
|
}> {
|
|
287
|
-
const blocksInCheckpoint:
|
|
528
|
+
const blocksInCheckpoint: L2Block[] = [];
|
|
288
529
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
289
530
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
290
531
|
|
|
291
532
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
292
|
-
let blockPendingBroadcast:
|
|
533
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
293
534
|
|
|
294
535
|
while (true) {
|
|
295
536
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
296
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
537
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
297
538
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
298
539
|
|
|
299
540
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
@@ -301,7 +542,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
301
542
|
|
|
302
543
|
if (!timingInfo.canStart) {
|
|
303
544
|
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
304
|
-
slot: this.
|
|
545
|
+
slot: this.targetSlot,
|
|
305
546
|
blocksBuilt,
|
|
306
547
|
secondsIntoSlot,
|
|
307
548
|
});
|
|
@@ -322,21 +563,23 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
322
563
|
txHashesAlreadyIncluded,
|
|
323
564
|
});
|
|
324
565
|
|
|
325
|
-
if
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
330
573
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
331
574
|
continue;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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) {
|
|
337
580
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
338
|
-
this.log.warn(`Halting block building for slot ${this.
|
|
339
|
-
slot: this.
|
|
581
|
+
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
582
|
+
slot: this.targetSlot,
|
|
340
583
|
blocksBuilt,
|
|
341
584
|
error: buildResult.error,
|
|
342
585
|
});
|
|
@@ -345,182 +588,261 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
345
588
|
}
|
|
346
589
|
|
|
347
590
|
const { block, usedTxs } = buildResult;
|
|
591
|
+
this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
|
|
592
|
+
isFirstBlock: blocksBuilt === 0,
|
|
593
|
+
isLastBlock: timingInfo.isLastBlock,
|
|
594
|
+
});
|
|
595
|
+
|
|
348
596
|
blocksInCheckpoint.push(block);
|
|
597
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
598
|
+
|
|
599
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
600
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
349
601
|
|
|
350
|
-
// Sync the proposed block to the archiver to make it available
|
|
351
|
-
//
|
|
352
|
-
//
|
|
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.
|
|
353
606
|
await this.syncProposedBlockToArchiver(block);
|
|
354
607
|
|
|
355
|
-
// 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.
|
|
356
609
|
if (timingInfo.isLastBlock) {
|
|
357
|
-
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.
|
|
358
|
-
slot: this.
|
|
610
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
611
|
+
slot: this.targetSlot,
|
|
359
612
|
blockNumber,
|
|
360
613
|
blocksBuilt,
|
|
361
614
|
});
|
|
362
|
-
|
|
615
|
+
|
|
616
|
+
blockPendingBroadcast = proposal;
|
|
363
617
|
break;
|
|
364
618
|
}
|
|
365
619
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
if (!this.config.fishermanMode) {
|
|
369
|
-
const proposal = await this.validatorClient.createBlockProposal(
|
|
370
|
-
block.header,
|
|
371
|
-
block.indexWithinCheckpoint,
|
|
372
|
-
inHash,
|
|
373
|
-
block.archive.root,
|
|
374
|
-
usedTxs,
|
|
375
|
-
this.proposer,
|
|
376
|
-
blockProposalOptions,
|
|
377
|
-
);
|
|
378
|
-
await this.p2pClient.broadcastProposal(proposal);
|
|
379
|
-
}
|
|
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));
|
|
380
622
|
|
|
381
623
|
// Wait until the next block's start time
|
|
382
624
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
383
625
|
}
|
|
384
626
|
|
|
385
|
-
this.log.verbose(`Block building loop completed for slot ${this.
|
|
386
|
-
slot: this.
|
|
627
|
+
this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
|
|
628
|
+
slot: this.targetSlot,
|
|
387
629
|
blocksBuilt: blocksInCheckpoint.length,
|
|
388
630
|
});
|
|
389
631
|
|
|
390
632
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
391
633
|
}
|
|
392
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
|
+
|
|
393
658
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
394
659
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
395
660
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
396
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.
|
|
397
|
-
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
|
+
});
|
|
398
665
|
await this.waitUntilTimeInSlot(nextSubslotStart);
|
|
399
666
|
}
|
|
400
667
|
|
|
401
668
|
/** Builds a single block. Called from the main block building loop. */
|
|
402
669
|
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
403
|
-
|
|
670
|
+
protected async buildSingleBlock(
|
|
404
671
|
checkpointBuilder: CheckpointBuilder,
|
|
405
672
|
opts: {
|
|
406
673
|
forceCreate?: boolean;
|
|
407
674
|
blockTimestamp: bigint;
|
|
408
675
|
blockNumber: BlockNumber;
|
|
409
|
-
indexWithinCheckpoint:
|
|
676
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
410
677
|
buildDeadline: Date | undefined;
|
|
411
678
|
txHashesAlreadyIncluded: Set<string>;
|
|
412
679
|
},
|
|
413
|
-
): Promise<
|
|
680
|
+
): Promise<
|
|
681
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
682
|
+
> {
|
|
414
683
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
415
684
|
opts;
|
|
416
685
|
|
|
417
686
|
this.log.verbose(
|
|
418
|
-
`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}`,
|
|
419
688
|
{ ...checkpointBuilder.getConstantData(), ...opts },
|
|
420
689
|
);
|
|
421
690
|
|
|
422
691
|
try {
|
|
423
692
|
// Wait until we have enough txs to build the block
|
|
424
|
-
const minTxs = this.
|
|
425
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
693
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
426
694
|
if (!canStartBuilding) {
|
|
427
695
|
this.log.warn(
|
|
428
|
-
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
429
|
-
{ 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 },
|
|
430
698
|
);
|
|
431
|
-
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 });
|
|
432
700
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
433
|
-
return
|
|
701
|
+
return { failure: 'insufficient-txs' };
|
|
434
702
|
}
|
|
435
703
|
|
|
436
704
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
437
705
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
438
706
|
const pendingTxs = filter(
|
|
439
|
-
this.p2pClient.
|
|
707
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
440
708
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
441
709
|
);
|
|
442
710
|
|
|
443
711
|
this.log.debug(
|
|
444
|
-
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.
|
|
445
|
-
{ slot: this.
|
|
712
|
+
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
|
|
713
|
+
{ slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
|
|
446
714
|
);
|
|
447
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.
|
|
448
|
-
|
|
715
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
716
|
+
|
|
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 = {
|
|
449
722
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
723
|
+
maxBlockGas:
|
|
724
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
725
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
726
|
+
: undefined,
|
|
453
727
|
deadline: buildDeadline,
|
|
728
|
+
isBuildingProposal: true,
|
|
729
|
+
minValidTxs,
|
|
730
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
731
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
454
732
|
};
|
|
455
733
|
|
|
456
|
-
// Actually build the block by executing txs
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
+
);
|
|
461
744
|
|
|
462
745
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
463
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
746
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
464
747
|
|
|
465
|
-
|
|
466
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
467
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
468
|
-
if (!forceCreate && numTxs < minValidTxs) {
|
|
748
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
469
749
|
this.log.warn(
|
|
470
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.
|
|
471
|
-
{
|
|
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
|
+
},
|
|
472
758
|
);
|
|
473
|
-
this.eventEmitter.emit('block-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
slot: this.slot,
|
|
759
|
+
this.eventEmitter.emit('block-build-failed', {
|
|
760
|
+
reason: `Insufficient valid txs`,
|
|
761
|
+
slot: this.targetSlot,
|
|
477
762
|
});
|
|
478
763
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
479
|
-
return
|
|
764
|
+
return { failure: 'insufficient-valid-txs' };
|
|
480
765
|
}
|
|
481
766
|
|
|
482
767
|
// Block creation succeeded, emit stats and metrics
|
|
768
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
769
|
+
|
|
483
770
|
const blockStats = {
|
|
484
771
|
eventName: 'l2-block-built',
|
|
485
772
|
duration: blockBuildDuration,
|
|
486
773
|
publicProcessDuration: publicProcessorDuration,
|
|
487
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
488
774
|
...block.getStats(),
|
|
489
775
|
} satisfies L2BlockBuiltStats;
|
|
490
776
|
|
|
491
777
|
const blockHash = await block.hash();
|
|
492
778
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
493
|
-
const manaPerSec =
|
|
779
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
494
780
|
|
|
495
781
|
this.log.info(
|
|
496
|
-
`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`,
|
|
497
783
|
{ blockHash, txHashes, manaPerSec, ...blockStats },
|
|
498
784
|
);
|
|
499
785
|
|
|
500
|
-
|
|
501
|
-
|
|
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);
|
|
502
794
|
|
|
503
795
|
return { block, usedTxs };
|
|
504
796
|
} catch (err: any) {
|
|
505
|
-
this.eventEmitter.emit('block-build-failed', {
|
|
506
|
-
|
|
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 });
|
|
507
802
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
508
803
|
this.metrics.recordFailedBlock();
|
|
509
804
|
return { error: err };
|
|
510
805
|
}
|
|
511
806
|
}
|
|
512
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
|
+
|
|
513
833
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
514
834
|
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
515
835
|
private async waitForMinTxs(opts: {
|
|
516
836
|
forceCreate?: boolean;
|
|
517
837
|
blockNumber: BlockNumber;
|
|
518
|
-
indexWithinCheckpoint:
|
|
838
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
519
839
|
buildDeadline: Date | undefined;
|
|
520
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
521
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
840
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
522
841
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
523
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
|
+
|
|
524
846
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
525
847
|
const startBuildingDeadline = buildDeadline
|
|
526
848
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -532,20 +854,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
532
854
|
// If we're past deadline, or we have no deadline, give up
|
|
533
855
|
const now = this.dateProvider.nowAsDate();
|
|
534
856
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
535
|
-
return { canStartBuilding: false, availableTxs
|
|
857
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
536
858
|
}
|
|
537
859
|
|
|
538
860
|
// Wait a bit before checking again
|
|
539
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.
|
|
861
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
540
862
|
this.log.verbose(
|
|
541
|
-
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.
|
|
542
|
-
{ 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 },
|
|
543
865
|
);
|
|
544
|
-
await
|
|
866
|
+
await this.waitForTxsPollingInterval();
|
|
545
867
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
546
868
|
}
|
|
547
869
|
|
|
548
|
-
return { canStartBuilding: true, availableTxs };
|
|
870
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
549
871
|
}
|
|
550
872
|
|
|
551
873
|
/**
|
|
@@ -571,18 +893,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
571
893
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
572
894
|
}
|
|
573
895
|
|
|
574
|
-
const numberOfRequiredAttestations =
|
|
896
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
575
897
|
|
|
576
898
|
if (this.config.skipCollectingAttestations) {
|
|
577
899
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
578
|
-
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
900
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
579
901
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
580
902
|
}
|
|
581
903
|
|
|
582
904
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
583
|
-
? this.timetable.
|
|
905
|
+
? this.timetable.getCheckpointAttestationDeadline()
|
|
584
906
|
: this.l1Constants.slotDuration;
|
|
585
|
-
const attestationDeadline = new Date(this.
|
|
907
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
586
908
|
|
|
587
909
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
588
910
|
|
|
@@ -593,15 +915,33 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
593
915
|
proposal,
|
|
594
916
|
numberOfRequiredAttestations,
|
|
595
917
|
attestationDeadline,
|
|
918
|
+
this.checkpointNumber,
|
|
596
919
|
);
|
|
597
920
|
|
|
598
921
|
collectedAttestationsCount = attestations.length;
|
|
599
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
|
+
|
|
600
935
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
601
|
-
const sorted = orderAttestations(
|
|
936
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
602
937
|
|
|
603
938
|
// Manipulate the attestations if we've been configured to do so
|
|
604
|
-
if (
|
|
939
|
+
if (
|
|
940
|
+
this.config.injectFakeAttestation ||
|
|
941
|
+
this.config.injectHighSValueAttestation ||
|
|
942
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
943
|
+
this.config.shuffleAttestationOrdering
|
|
944
|
+
) {
|
|
605
945
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
606
946
|
}
|
|
607
947
|
|
|
@@ -630,7 +970,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
630
970
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
631
971
|
);
|
|
632
972
|
|
|
633
|
-
if (
|
|
973
|
+
if (
|
|
974
|
+
this.config.injectFakeAttestation ||
|
|
975
|
+
this.config.injectHighSValueAttestation ||
|
|
976
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
977
|
+
) {
|
|
634
978
|
// Find non-empty attestations that are not from the proposer
|
|
635
979
|
const nonProposerIndices: number[] = [];
|
|
636
980
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -640,8 +984,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
640
984
|
}
|
|
641
985
|
if (nonProposerIndices.length > 0) {
|
|
642
986
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
643
|
-
this.
|
|
644
|
-
|
|
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
|
+
}
|
|
645
1001
|
}
|
|
646
1002
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
647
1003
|
}
|
|
@@ -650,11 +1006,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
650
1006
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
651
1007
|
|
|
652
1008
|
const shuffled = [...attestations];
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
}
|
|
658
1023
|
|
|
659
1024
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
660
1025
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -670,17 +1035,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
670
1035
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
671
1036
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
672
1037
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
673
|
-
await this.p2pClient.
|
|
1038
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
674
1039
|
}
|
|
675
1040
|
|
|
676
1041
|
/**
|
|
677
1042
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
678
1043
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
679
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.
|
|
680
1049
|
*/
|
|
681
|
-
private async syncProposedBlockToArchiver(block:
|
|
682
|
-
|
|
683
|
-
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
1050
|
+
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
1051
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
684
1052
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
685
1053
|
blockNumber: block.number,
|
|
686
1054
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -698,27 +1066,49 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
698
1066
|
private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
|
|
699
1067
|
// Perform L1 fee analysis before clearing requests
|
|
700
1068
|
// The callback is invoked asynchronously after the next block is mined
|
|
701
|
-
const feeAnalysis = await this.publisher.analyzeL1Fees(this.
|
|
1069
|
+
const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, analysis =>
|
|
702
1070
|
this.metrics.recordFishermanFeeAnalysis(analysis),
|
|
703
1071
|
);
|
|
704
1072
|
|
|
705
1073
|
if (checkpoint) {
|
|
706
|
-
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.
|
|
1074
|
+
this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
|
|
707
1075
|
...checkpoint.toCheckpointInfo(),
|
|
708
1076
|
...checkpoint.getStats(),
|
|
709
1077
|
feeAnalysisId: feeAnalysis?.id,
|
|
710
1078
|
});
|
|
711
1079
|
} else {
|
|
712
|
-
this.log.warn(`Validation block building FAILED for slot ${this.
|
|
713
|
-
slot: this.
|
|
1080
|
+
this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
|
|
1081
|
+
slot: this.targetSlot,
|
|
714
1082
|
feeAnalysisId: feeAnalysis?.id,
|
|
715
1083
|
});
|
|
716
|
-
this.metrics.
|
|
1084
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
717
1085
|
}
|
|
718
1086
|
|
|
719
1087
|
this.publisher.clearPendingRequests();
|
|
720
1088
|
}
|
|
721
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
|
+
|
|
722
1112
|
/** Waits until a specific time within the current slot */
|
|
723
1113
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
724
1114
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -727,8 +1117,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
727
1117
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
728
1118
|
}
|
|
729
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
|
+
|
|
730
1125
|
private getSlotStartBuildTimestamp(): number {
|
|
731
|
-
return getSlotStartBuildTimestamp(this.
|
|
1126
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
732
1127
|
}
|
|
733
1128
|
|
|
734
1129
|
private getSecondsIntoSlot(): number {
|