@aztec/sequencer-client 0.0.1-commit.03f7ef2 → 0.0.1-commit.04852196a
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 +26 -11
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +99 -16
- package/dest/config.d.ts +24 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +45 -28
- package/dest/global_variable_builder/global_builder.d.ts +5 -7
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +13 -13
- package/dest/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/publisher/config.d.ts +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +12 -4
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -2
- 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 +23 -86
- package/dest/publisher/sequencer-publisher.d.ts +44 -25
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +781 -101
- package/dest/sequencer/checkpoint_proposal_job.d.ts +39 -13
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +683 -79
- package/dest/sequencer/checkpoint_voter.d.ts +3 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -10
- package/dest/sequencer/index.d.ts +1 -3
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -2
- package/dest/sequencer/metrics.d.ts +19 -7
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +131 -141
- package/dest/sequencer/sequencer.d.ts +46 -23
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +514 -67
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +5 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +4 -7
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +28 -16
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +86 -34
- package/dest/test/utils.d.ts +13 -9
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +27 -17
- package/package.json +30 -28
- package/src/client/sequencer-client.ts +139 -23
- package/src/config.ts +59 -38
- package/src/global_variable_builder/global_builder.ts +14 -14
- package/src/index.ts +1 -9
- package/src/publisher/config.ts +121 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +39 -7
- package/src/publisher/sequencer-publisher-metrics.ts +17 -69
- package/src/publisher/sequencer-publisher.ts +420 -137
- package/src/sequencer/checkpoint_proposal_job.ts +361 -104
- package/src/sequencer/checkpoint_voter.ts +32 -7
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/metrics.ts +132 -148
- package/src/sequencer/sequencer.ts +160 -69
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +4 -1
- package/src/test/index.ts +3 -6
- package/src/test/mock_checkpoint_builder.ts +147 -71
- package/src/test/utils.ts +58 -28
- 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/dest/sequencer/checkpoint_builder.d.ts +0 -63
- package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
- package/dest/sequencer/checkpoint_builder.js +0 -131
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -53
- package/src/sequencer/block_builder.ts +0 -217
- package/src/sequencer/checkpoint_builder.ts +0 -217
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -133
|
@@ -1,42 +1,56 @@
|
|
|
1
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
2
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BlockNumber,
|
|
4
|
+
CheckpointNumber,
|
|
5
|
+
EpochNumber,
|
|
6
|
+
IndexWithinCheckpoint,
|
|
7
|
+
SlotNumber,
|
|
8
|
+
} from '@aztec/foundation/branded-types';
|
|
4
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
15
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
7
18
|
import { filter } from '@aztec/foundation/iterator';
|
|
8
|
-
import type
|
|
19
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
9
20
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
10
21
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
11
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
22
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
12
23
|
import type { P2P } from '@aztec/p2p';
|
|
13
24
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
14
25
|
import {
|
|
15
26
|
CommitteeAttestation,
|
|
16
27
|
CommitteeAttestationsAndSigners,
|
|
17
|
-
|
|
28
|
+
L2Block,
|
|
29
|
+
type L2BlockSink,
|
|
30
|
+
type L2BlockSource,
|
|
18
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
19
32
|
} from '@aztec/stdlib/block';
|
|
20
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
21
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
22
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
import {
|
|
37
|
+
NoValidTxsError,
|
|
38
|
+
type PublicProcessorLimits,
|
|
39
|
+
type ResolvedSequencerConfig,
|
|
40
|
+
type WorldStateSynchronizer,
|
|
27
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
28
|
-
import type
|
|
29
|
-
import type {
|
|
30
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
31
|
-
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
42
|
+
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
43
|
+
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
44
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
32
45
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
33
46
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
34
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
35
|
-
import type
|
|
48
|
+
import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
|
|
49
|
+
import { CheckpointBuilder, type FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
|
|
50
|
+
import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
|
|
36
51
|
|
|
37
52
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
38
|
-
import type {
|
|
39
|
-
import { CheckpointBuilder, type FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
53
|
+
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
40
54
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
41
55
|
import { SequencerInterruptedError } from './errors.js';
|
|
42
56
|
import type { SequencerEvents } from './events.js';
|
|
@@ -54,8 +68,11 @@ const TXS_POLLING_MS = 500;
|
|
|
54
68
|
* as well as enqueueing votes for slashing and governance proposals. This class is created from
|
|
55
69
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
56
70
|
*/
|
|
57
|
-
export class CheckpointProposalJob {
|
|
71
|
+
export class CheckpointProposalJob implements Traceable {
|
|
72
|
+
protected readonly log: Logger;
|
|
73
|
+
|
|
58
74
|
constructor(
|
|
75
|
+
private readonly epoch: EpochNumber,
|
|
59
76
|
private readonly slot: SlotNumber,
|
|
60
77
|
private readonly checkpointNumber: CheckpointNumber,
|
|
61
78
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
@@ -63,13 +80,15 @@ export class CheckpointProposalJob {
|
|
|
63
80
|
private readonly proposer: EthAddress | undefined,
|
|
64
81
|
private readonly publisher: SequencerPublisher,
|
|
65
82
|
private readonly attestorAddress: EthAddress,
|
|
66
|
-
private readonly
|
|
83
|
+
private readonly invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
67
84
|
private readonly validatorClient: ValidatorClient,
|
|
68
85
|
private readonly globalsBuilder: GlobalVariableBuilder,
|
|
69
86
|
private readonly p2pClient: P2P,
|
|
70
87
|
private readonly worldState: WorldStateSynchronizer,
|
|
71
88
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
89
|
+
private readonly l2BlockSource: L2BlockSource,
|
|
72
90
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
91
|
+
private readonly blockSink: L2BlockSink,
|
|
73
92
|
private readonly l1Constants: SequencerRollupConstants,
|
|
74
93
|
protected config: ResolvedSequencerConfig,
|
|
75
94
|
protected timetable: SequencerTimetable,
|
|
@@ -79,13 +98,17 @@ export class CheckpointProposalJob {
|
|
|
79
98
|
private readonly metrics: SequencerMetrics,
|
|
80
99
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
81
100
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
public readonly tracer: Tracer,
|
|
102
|
+
bindings?: LoggerBindings,
|
|
103
|
+
) {
|
|
104
|
+
this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
|
|
105
|
+
}
|
|
84
106
|
|
|
85
107
|
/**
|
|
86
108
|
* Executes the checkpoint proposal job.
|
|
87
109
|
* Returns the published checkpoint if successful, undefined otherwise.
|
|
88
110
|
*/
|
|
111
|
+
@trackSpan('CheckpointProposalJob.execute')
|
|
89
112
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
90
113
|
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
91
114
|
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
@@ -108,6 +131,10 @@ export class CheckpointProposalJob {
|
|
|
108
131
|
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
109
132
|
await Promise.all(votesPromises);
|
|
110
133
|
|
|
134
|
+
if (checkpoint) {
|
|
135
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
112
139
|
if (this.config.fishermanMode) {
|
|
113
140
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
@@ -128,6 +155,13 @@ export class CheckpointProposalJob {
|
|
|
128
155
|
}
|
|
129
156
|
}
|
|
130
157
|
|
|
158
|
+
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
159
|
+
return {
|
|
160
|
+
// nullish operator needed for tests
|
|
161
|
+
[Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
|
|
162
|
+
[Attributes.SLOT_NUMBER]: this.slot,
|
|
163
|
+
};
|
|
164
|
+
})
|
|
131
165
|
private async proposeCheckpoint(): Promise<Checkpoint | undefined> {
|
|
132
166
|
try {
|
|
133
167
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
@@ -138,9 +172,9 @@ export class CheckpointProposalJob {
|
|
|
138
172
|
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.slot);
|
|
139
173
|
this.metrics.incOpenSlot(this.slot, this.proposer?.toString() ?? 'unknown');
|
|
140
174
|
|
|
141
|
-
// Enqueues
|
|
142
|
-
if (this.
|
|
143
|
-
this.publisher.
|
|
175
|
+
// Enqueues checkpoint invalidation (constant for the whole slot)
|
|
176
|
+
if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
|
|
177
|
+
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
144
178
|
}
|
|
145
179
|
|
|
146
180
|
// Create checkpoint builder for the slot
|
|
@@ -150,18 +184,30 @@ export class CheckpointProposalJob {
|
|
|
150
184
|
this.slot,
|
|
151
185
|
);
|
|
152
186
|
|
|
153
|
-
// Collect L1 to L2 messages for the checkpoint
|
|
187
|
+
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
154
188
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
189
|
+
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
190
|
+
|
|
191
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
192
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
193
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
194
|
+
.map(c => c.checkpointOutHash);
|
|
195
|
+
|
|
196
|
+
// Get the fee asset price modifier from the oracle
|
|
197
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
155
198
|
|
|
156
199
|
// Create a long-lived forked world state for the checkpoint builder
|
|
157
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
200
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
158
201
|
|
|
159
202
|
// Create checkpoint builder for the entire slot
|
|
160
203
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
161
204
|
this.checkpointNumber,
|
|
162
205
|
checkpointGlobalVariables,
|
|
206
|
+
feeAssetPriceModifier,
|
|
163
207
|
l1ToL2Messages,
|
|
208
|
+
previousCheckpointOutHashes,
|
|
164
209
|
fork,
|
|
210
|
+
this.log.getBindings(),
|
|
165
211
|
);
|
|
166
212
|
|
|
167
213
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -170,12 +216,34 @@ export class CheckpointProposalJob {
|
|
|
170
216
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
171
217
|
};
|
|
172
218
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
const checkpointProposalOptions: CheckpointProposalOptions = {
|
|
220
|
+
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
221
|
+
broadcastInvalidCheckpointProposal: this.config.broadcastInvalidBlockProposal,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let blocksInCheckpoint: L2Block[] = [];
|
|
225
|
+
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
226
|
+
const checkpointBuildTimer = new Timer();
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// Main loop: build blocks for the checkpoint
|
|
230
|
+
const result = await this.buildBlocksForCheckpoint(
|
|
231
|
+
checkpointBuilder,
|
|
232
|
+
checkpointGlobalVariables.timestamp,
|
|
233
|
+
inHash,
|
|
234
|
+
blockProposalOptions,
|
|
235
|
+
);
|
|
236
|
+
blocksInCheckpoint = result.blocksInCheckpoint;
|
|
237
|
+
blockPendingBroadcast = result.blockPendingBroadcast;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
240
|
+
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
241
|
+
// which is normal for block building (may have picked different txs)
|
|
242
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
179
247
|
|
|
180
248
|
if (blocksInCheckpoint.length === 0) {
|
|
181
249
|
this.log.warn(`No blocks were built for slot ${this.slot}`, { slot: this.slot });
|
|
@@ -183,11 +251,44 @@ export class CheckpointProposalJob {
|
|
|
183
251
|
return undefined;
|
|
184
252
|
}
|
|
185
253
|
|
|
254
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
255
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
256
|
+
this.log.warn(
|
|
257
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
258
|
+
{ slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
259
|
+
);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
186
263
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
187
264
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
188
265
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
189
266
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
190
267
|
|
|
268
|
+
// Final validation round for the checkpoint before we propose it, just for safety
|
|
269
|
+
try {
|
|
270
|
+
validateCheckpoint(checkpoint, {
|
|
271
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
272
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
273
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
274
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
275
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
279
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
280
|
+
});
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Record checkpoint-level build metrics
|
|
285
|
+
this.metrics.recordCheckpointBuild(
|
|
286
|
+
checkpointBuildTimer.ms(),
|
|
287
|
+
blocksInCheckpoint.length,
|
|
288
|
+
checkpoint.getStats().txCount,
|
|
289
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
290
|
+
);
|
|
291
|
+
|
|
191
292
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
192
293
|
if (this.config.fishermanMode) {
|
|
193
294
|
this.log.info(
|
|
@@ -203,39 +304,83 @@ export class CheckpointProposalJob {
|
|
|
203
304
|
return checkpoint;
|
|
204
305
|
}
|
|
205
306
|
|
|
206
|
-
//
|
|
307
|
+
// Include the block pending broadcast in the checkpoint proposal if any
|
|
308
|
+
const lastBlock = blockPendingBroadcast && {
|
|
309
|
+
blockHeader: blockPendingBroadcast.block.header,
|
|
310
|
+
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
311
|
+
txs: blockPendingBroadcast.txs,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Create the checkpoint proposal and broadcast it
|
|
207
315
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
208
316
|
checkpoint.header,
|
|
209
317
|
checkpoint.archive.root,
|
|
210
|
-
|
|
318
|
+
feeAssetPriceModifier,
|
|
319
|
+
lastBlock,
|
|
211
320
|
this.proposer,
|
|
212
|
-
|
|
321
|
+
checkpointProposalOptions,
|
|
213
322
|
);
|
|
323
|
+
|
|
214
324
|
const blockProposedAt = this.dateProvider.now();
|
|
215
|
-
await this.p2pClient.
|
|
325
|
+
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
216
326
|
|
|
217
327
|
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.slot);
|
|
218
328
|
const attestations = await this.waitForAttestations(proposal);
|
|
219
329
|
const blockAttestedAt = this.dateProvider.now();
|
|
220
330
|
|
|
221
|
-
this.metrics.
|
|
331
|
+
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
222
332
|
|
|
223
333
|
// Proposer must sign over the attestations before pushing them to L1
|
|
224
334
|
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
225
|
-
|
|
335
|
+
let attestationsSignature: Signature;
|
|
336
|
+
try {
|
|
337
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
338
|
+
attestations,
|
|
339
|
+
signer,
|
|
340
|
+
this.slot,
|
|
341
|
+
this.checkpointNumber,
|
|
342
|
+
);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// We shouldn't really get here since we yield to another HA node
|
|
345
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
346
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
226
351
|
|
|
227
352
|
// Enqueue publishing the checkpoint to L1
|
|
228
353
|
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
|
|
229
354
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
230
355
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
231
356
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
357
|
+
|
|
358
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
359
|
+
if (
|
|
360
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
361
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
362
|
+
) {
|
|
363
|
+
const result = Math.max(0, randomInt(100));
|
|
364
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
365
|
+
this.log.warn(
|
|
366
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
367
|
+
);
|
|
368
|
+
return checkpoint;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
232
372
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
233
373
|
txTimeoutAt,
|
|
234
|
-
|
|
374
|
+
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
235
375
|
});
|
|
236
376
|
|
|
237
377
|
return checkpoint;
|
|
238
378
|
} catch (err) {
|
|
379
|
+
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
380
|
+
// swallow this error. It's already been logged by a function deeper in the stack
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
239
384
|
this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
|
|
240
385
|
return undefined;
|
|
241
386
|
}
|
|
@@ -244,24 +389,26 @@ export class CheckpointProposalJob {
|
|
|
244
389
|
/**
|
|
245
390
|
* Builds blocks for a checkpoint within the current slot.
|
|
246
391
|
*/
|
|
392
|
+
@trackSpan('CheckpointProposalJob.buildBlocksForCheckpoint')
|
|
247
393
|
private async buildBlocksForCheckpoint(
|
|
248
394
|
checkpointBuilder: CheckpointBuilder,
|
|
249
395
|
timestamp: bigint,
|
|
396
|
+
inHash: Fr,
|
|
250
397
|
blockProposalOptions: BlockProposalOptions,
|
|
251
398
|
): Promise<{
|
|
252
|
-
blocksInCheckpoint:
|
|
253
|
-
|
|
399
|
+
blocksInCheckpoint: L2Block[];
|
|
400
|
+
blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined;
|
|
254
401
|
}> {
|
|
255
|
-
const blocksInCheckpoint:
|
|
402
|
+
const blocksInCheckpoint: L2Block[] = [];
|
|
256
403
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
257
404
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
258
405
|
|
|
259
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
260
|
-
let
|
|
407
|
+
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
261
408
|
|
|
262
409
|
while (true) {
|
|
263
410
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
264
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
411
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
265
412
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
266
413
|
|
|
267
414
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
@@ -290,6 +437,7 @@ export class CheckpointProposalJob {
|
|
|
290
437
|
txHashesAlreadyIncluded,
|
|
291
438
|
});
|
|
292
439
|
|
|
440
|
+
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
293
441
|
if (!buildResult && timingInfo.isLastBlock) {
|
|
294
442
|
// If no block was produced due to not enough txs and this was the last subslot, exit
|
|
295
443
|
break;
|
|
@@ -318,7 +466,12 @@ export class CheckpointProposalJob {
|
|
|
318
466
|
// Sync the proposed block to the archiver to make it available
|
|
319
467
|
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
320
468
|
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
321
|
-
|
|
469
|
+
// Fire and forget - don't block the critical path, but log errors
|
|
470
|
+
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
471
|
+
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
322
475
|
|
|
323
476
|
// If this is the last block, exit the loop now so we start collecting attestations
|
|
324
477
|
if (timingInfo.isLastBlock) {
|
|
@@ -327,17 +480,17 @@ export class CheckpointProposalJob {
|
|
|
327
480
|
blockNumber,
|
|
328
481
|
blocksBuilt,
|
|
329
482
|
});
|
|
330
|
-
|
|
483
|
+
blockPendingBroadcast = { block, txs: usedTxs };
|
|
331
484
|
break;
|
|
332
485
|
}
|
|
333
486
|
|
|
334
487
|
// For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
|
|
335
488
|
// If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
|
|
336
489
|
if (!this.config.fishermanMode) {
|
|
337
|
-
// TODO(palla/mbps): Wire this to the new p2p API once available
|
|
338
490
|
const proposal = await this.validatorClient.createBlockProposal(
|
|
339
|
-
block.header
|
|
340
|
-
|
|
491
|
+
block.header,
|
|
492
|
+
block.indexWithinCheckpoint,
|
|
493
|
+
inHash,
|
|
341
494
|
block.archive.root,
|
|
342
495
|
usedTxs,
|
|
343
496
|
this.proposer,
|
|
@@ -355,13 +508,11 @@ export class CheckpointProposalJob {
|
|
|
355
508
|
blocksBuilt: blocksInCheckpoint.length,
|
|
356
509
|
});
|
|
357
510
|
|
|
358
|
-
return {
|
|
359
|
-
blocksInCheckpoint,
|
|
360
|
-
pendingBroadcast,
|
|
361
|
-
};
|
|
511
|
+
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
362
512
|
}
|
|
363
513
|
|
|
364
514
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
515
|
+
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
365
516
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
366
517
|
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.slot);
|
|
367
518
|
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, { slot: this.slot });
|
|
@@ -369,17 +520,18 @@ export class CheckpointProposalJob {
|
|
|
369
520
|
}
|
|
370
521
|
|
|
371
522
|
/** Builds a single block. Called from the main block building loop. */
|
|
372
|
-
|
|
523
|
+
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
524
|
+
protected async buildSingleBlock(
|
|
373
525
|
checkpointBuilder: CheckpointBuilder,
|
|
374
526
|
opts: {
|
|
375
527
|
forceCreate?: boolean;
|
|
376
528
|
blockTimestamp: bigint;
|
|
377
529
|
blockNumber: BlockNumber;
|
|
378
|
-
indexWithinCheckpoint:
|
|
530
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
379
531
|
buildDeadline: Date | undefined;
|
|
380
532
|
txHashesAlreadyIncluded: Set<string>;
|
|
381
533
|
},
|
|
382
|
-
): Promise<{ block:
|
|
534
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
383
535
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
384
536
|
opts;
|
|
385
537
|
|
|
@@ -405,7 +557,7 @@ export class CheckpointProposalJob {
|
|
|
405
557
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
406
558
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
407
559
|
const pendingTxs = filter(
|
|
408
|
-
this.p2pClient.
|
|
560
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
409
561
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
410
562
|
);
|
|
411
563
|
|
|
@@ -414,52 +566,58 @@ export class CheckpointProposalJob {
|
|
|
414
566
|
{ slot: this.slot, blockNumber, indexWithinCheckpoint },
|
|
415
567
|
);
|
|
416
568
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
569
|
+
|
|
570
|
+
// Per-block limits derived at startup by computeBlockLimits(), further capped
|
|
571
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
417
572
|
const blockBuilderOptions: PublicProcessorLimits = {
|
|
418
573
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
574
|
+
maxBlockGas:
|
|
575
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
576
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
577
|
+
: undefined,
|
|
422
578
|
deadline: buildDeadline,
|
|
579
|
+
isBuildingProposal: true,
|
|
423
580
|
};
|
|
424
581
|
|
|
425
582
|
// Actually build the block by executing txs
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
583
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
584
|
+
checkpointBuilder,
|
|
585
|
+
pendingTxs,
|
|
586
|
+
blockNumber,
|
|
587
|
+
blockTimestamp,
|
|
588
|
+
blockBuilderOptions,
|
|
589
|
+
);
|
|
430
590
|
|
|
431
591
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
432
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
592
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
433
593
|
|
|
434
594
|
// Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
|
|
435
595
|
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
436
596
|
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
437
|
-
|
|
597
|
+
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
598
|
+
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
438
599
|
this.log.warn(
|
|
439
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed
|
|
440
|
-
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
|
|
600
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
601
|
+
{ slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
|
|
441
602
|
);
|
|
442
|
-
this.eventEmitter.emit('block-
|
|
443
|
-
minTxs: minValidTxs,
|
|
444
|
-
availableTxs: numTxs,
|
|
445
|
-
slot: this.slot,
|
|
446
|
-
});
|
|
603
|
+
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
447
604
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
448
605
|
return undefined;
|
|
449
606
|
}
|
|
450
607
|
|
|
451
608
|
// Block creation succeeded, emit stats and metrics
|
|
609
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration } = buildResult;
|
|
610
|
+
|
|
452
611
|
const blockStats = {
|
|
453
612
|
eventName: 'l2-block-built',
|
|
454
613
|
duration: blockBuildDuration,
|
|
455
614
|
publicProcessDuration: publicProcessorDuration,
|
|
456
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
457
615
|
...block.getStats(),
|
|
458
616
|
} satisfies L2BlockBuiltStats;
|
|
459
617
|
|
|
460
618
|
const blockHash = await block.hash();
|
|
461
619
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
462
|
-
const manaPerSec =
|
|
620
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
463
621
|
|
|
464
622
|
this.log.info(
|
|
465
623
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -467,7 +625,7 @@ export class CheckpointProposalJob {
|
|
|
467
625
|
);
|
|
468
626
|
|
|
469
627
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
470
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
628
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
471
629
|
|
|
472
630
|
return { block, usedTxs };
|
|
473
631
|
} catch (err: any) {
|
|
@@ -479,16 +637,40 @@ export class CheckpointProposalJob {
|
|
|
479
637
|
}
|
|
480
638
|
}
|
|
481
639
|
|
|
640
|
+
/** Uses the checkpoint builder to build a block, catching specific txs */
|
|
641
|
+
private async buildSingleBlockWithCheckpointBuilder(
|
|
642
|
+
checkpointBuilder: CheckpointBuilder,
|
|
643
|
+
pendingTxs: AsyncIterable<Tx>,
|
|
644
|
+
blockNumber: BlockNumber,
|
|
645
|
+
blockTimestamp: bigint,
|
|
646
|
+
blockBuilderOptions: PublicProcessorLimits,
|
|
647
|
+
) {
|
|
648
|
+
try {
|
|
649
|
+
const workTimer = new Timer();
|
|
650
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
651
|
+
const blockBuildDuration = workTimer.ms();
|
|
652
|
+
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
653
|
+
} catch (err: unknown) {
|
|
654
|
+
if (isErrorClass(err, NoValidTxsError)) {
|
|
655
|
+
return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
|
|
656
|
+
}
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
482
661
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
662
|
+
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
483
663
|
private async waitForMinTxs(opts: {
|
|
484
664
|
forceCreate?: boolean;
|
|
485
665
|
blockNumber: BlockNumber;
|
|
486
|
-
indexWithinCheckpoint:
|
|
666
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
487
667
|
buildDeadline: Date | undefined;
|
|
488
668
|
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
489
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
490
669
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
491
670
|
|
|
671
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
672
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
673
|
+
|
|
492
674
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
493
675
|
const startBuildingDeadline = buildDeadline
|
|
494
676
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -509,7 +691,7 @@ export class CheckpointProposalJob {
|
|
|
509
691
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
510
692
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
511
693
|
);
|
|
512
|
-
await
|
|
694
|
+
await this.waitForTxsPollingInterval();
|
|
513
695
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
514
696
|
}
|
|
515
697
|
|
|
@@ -520,7 +702,8 @@ export class CheckpointProposalJob {
|
|
|
520
702
|
* Waits for enough attestations to be collected via p2p.
|
|
521
703
|
* This is run after all blocks for the checkpoint have been built.
|
|
522
704
|
*/
|
|
523
|
-
|
|
705
|
+
@trackSpan('CheckpointProposalJob.waitForAttestations')
|
|
706
|
+
private async waitForAttestations(proposal: CheckpointProposal): Promise<CommitteeAttestationsAndSigners> {
|
|
524
707
|
if (this.config.fishermanMode) {
|
|
525
708
|
this.log.debug('Skipping attestation collection in fisherman mode');
|
|
526
709
|
return CommitteeAttestationsAndSigners.empty();
|
|
@@ -549,7 +732,7 @@ export class CheckpointProposalJob {
|
|
|
549
732
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
550
733
|
? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
|
|
551
734
|
: this.l1Constants.slotDuration;
|
|
552
|
-
const attestationDeadline = new Date(this.
|
|
735
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
553
736
|
|
|
554
737
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
555
738
|
|
|
@@ -564,13 +747,29 @@ export class CheckpointProposalJob {
|
|
|
564
747
|
|
|
565
748
|
collectedAttestationsCount = attestations.length;
|
|
566
749
|
|
|
750
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
751
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
752
|
+
const trimmed = trimAttestations(
|
|
753
|
+
attestations,
|
|
754
|
+
numberOfRequiredAttestations,
|
|
755
|
+
this.attestorAddress,
|
|
756
|
+
localAddresses,
|
|
757
|
+
);
|
|
758
|
+
if (trimmed.length < attestations.length) {
|
|
759
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
760
|
+
}
|
|
761
|
+
|
|
567
762
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
568
|
-
const sorted = orderAttestations(
|
|
763
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
569
764
|
|
|
570
765
|
// Manipulate the attestations if we've been configured to do so
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
766
|
+
if (
|
|
767
|
+
this.config.injectFakeAttestation ||
|
|
768
|
+
this.config.injectHighSValueAttestation ||
|
|
769
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
770
|
+
this.config.shuffleAttestationOrdering
|
|
771
|
+
) {
|
|
772
|
+
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
574
773
|
}
|
|
575
774
|
|
|
576
775
|
return new CommitteeAttestationsAndSigners(sorted);
|
|
@@ -586,7 +785,7 @@ export class CheckpointProposalJob {
|
|
|
586
785
|
|
|
587
786
|
/** Breaks the attestations before publishing based on attack configs */
|
|
588
787
|
private manipulateAttestations(
|
|
589
|
-
|
|
788
|
+
slotNumber: SlotNumber,
|
|
590
789
|
epoch: EpochNumber,
|
|
591
790
|
seed: bigint,
|
|
592
791
|
committee: EthAddress[],
|
|
@@ -594,12 +793,15 @@ export class CheckpointProposalJob {
|
|
|
594
793
|
) {
|
|
595
794
|
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
596
795
|
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
597
|
-
const { slotNumber } = checkpoint;
|
|
598
796
|
const proposerIndex = Number(
|
|
599
797
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
600
798
|
);
|
|
601
799
|
|
|
602
|
-
if (
|
|
800
|
+
if (
|
|
801
|
+
this.config.injectFakeAttestation ||
|
|
802
|
+
this.config.injectHighSValueAttestation ||
|
|
803
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
804
|
+
) {
|
|
603
805
|
// Find non-empty attestations that are not from the proposer
|
|
604
806
|
const nonProposerIndices: number[] = [];
|
|
605
807
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -609,8 +811,20 @@ export class CheckpointProposalJob {
|
|
|
609
811
|
}
|
|
610
812
|
if (nonProposerIndices.length > 0) {
|
|
611
813
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
612
|
-
this.
|
|
613
|
-
|
|
814
|
+
if (this.config.injectHighSValueAttestation) {
|
|
815
|
+
this.log.warn(
|
|
816
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
817
|
+
);
|
|
818
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
819
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
820
|
+
this.log.warn(
|
|
821
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
822
|
+
);
|
|
823
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
824
|
+
} else {
|
|
825
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
826
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
827
|
+
}
|
|
614
828
|
}
|
|
615
829
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
616
830
|
}
|
|
@@ -619,11 +833,20 @@ export class CheckpointProposalJob {
|
|
|
619
833
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
620
834
|
|
|
621
835
|
const shuffled = [...attestations];
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
836
|
+
|
|
837
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
838
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
839
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
840
|
+
const swappable: number[] = [];
|
|
841
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
842
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
843
|
+
swappable.push(k);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (swappable.length >= 2) {
|
|
847
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
848
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
849
|
+
}
|
|
627
850
|
|
|
628
851
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
629
852
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -639,20 +862,27 @@ export class CheckpointProposalJob {
|
|
|
639
862
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
640
863
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
641
864
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
642
|
-
await this.p2pClient.
|
|
865
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
643
866
|
}
|
|
644
867
|
|
|
645
868
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
869
|
+
* Adds the proposed block to the archiver so it's available via P2P.
|
|
870
|
+
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
871
|
+
* would never receive its own block without this explicit sync.
|
|
648
872
|
*/
|
|
649
|
-
private async syncProposedBlockToArchiver(block:
|
|
650
|
-
this.
|
|
873
|
+
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
874
|
+
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
875
|
+
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
876
|
+
blockNumber: block.number,
|
|
877
|
+
slot: block.header.globalVariables.slotNumber,
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
this.log.debug(`Syncing proposed block ${block.number} to archiver`, {
|
|
651
882
|
blockNumber: block.number,
|
|
652
883
|
slot: block.header.globalVariables.slotNumber,
|
|
653
884
|
});
|
|
654
|
-
|
|
655
|
-
await Promise.resolve();
|
|
885
|
+
await this.blockSink.addBlock(block);
|
|
656
886
|
}
|
|
657
887
|
|
|
658
888
|
/** Runs fee analysis and logs checkpoint outcome as fisherman */
|
|
@@ -669,25 +899,52 @@ export class CheckpointProposalJob {
|
|
|
669
899
|
...checkpoint.getStats(),
|
|
670
900
|
feeAnalysisId: feeAnalysis?.id,
|
|
671
901
|
});
|
|
672
|
-
this.metrics.recordBlockProposalSuccess();
|
|
673
902
|
} else {
|
|
674
903
|
this.log.warn(`Validation block building FAILED for slot ${this.slot}`, {
|
|
675
904
|
slot: this.slot,
|
|
676
905
|
feeAnalysisId: feeAnalysis?.id,
|
|
677
906
|
});
|
|
678
|
-
this.metrics.
|
|
907
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
679
908
|
}
|
|
680
909
|
|
|
681
910
|
this.publisher.clearPendingRequests();
|
|
682
911
|
}
|
|
683
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
915
|
+
*/
|
|
916
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
917
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
918
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
919
|
+
slot: this.slot,
|
|
920
|
+
signedByNode: err.signedByNode,
|
|
921
|
+
});
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
if (err instanceof SlashingProtectionError) {
|
|
925
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
926
|
+
slot: this.slot,
|
|
927
|
+
existingMessageHash: err.existingMessageHash,
|
|
928
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
929
|
+
});
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
|
|
684
935
|
/** Waits until a specific time within the current slot */
|
|
936
|
+
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
685
937
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
686
938
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
687
939
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
688
940
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
689
941
|
}
|
|
690
942
|
|
|
943
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
944
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
945
|
+
await sleep(TXS_POLLING_MS);
|
|
946
|
+
}
|
|
947
|
+
|
|
691
948
|
private getSlotStartBuildTimestamp(): number {
|
|
692
949
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
693
950
|
}
|