@aztec/sequencer-client 3.0.0-canary.a9708bd → 3.0.0-devnet.20251212
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/index.d.ts +1 -1
- package/dest/client/sequencer-client.d.ts +7 -6
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +23 -14
- package/dest/config.d.ts +5 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +21 -1
- 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 +12 -8
- package/dest/global_variable_builder/index.d.ts +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/publisher/config.d.ts +9 -10
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +19 -17
- package/dest/publisher/index.d.ts +2 -2
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +9 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +8 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts +2 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +53 -50
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +187 -132
- package/dest/sequencer/block_builder.d.ts +6 -8
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +18 -9
- package/dest/sequencer/config.d.ts +2 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +11 -0
- package/dest/sequencer/errors.d.ts.map +1 -0
- package/dest/sequencer/errors.js +15 -0
- package/dest/sequencer/index.d.ts +1 -1
- package/dest/sequencer/metrics.d.ts +17 -20
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +59 -87
- package/dest/sequencer/sequencer.d.ts +54 -32
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +458 -184
- package/dest/sequencer/timetable.d.ts +4 -8
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +3 -10
- package/dest/sequencer/utils.d.ts +11 -25
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +9 -24
- package/dest/test/index.d.ts +2 -2
- package/dest/test/index.d.ts.map +1 -1
- package/dest/tx_validator/nullifier_cache.d.ts +1 -1
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +12 -9
- package/package.json +32 -31
- package/src/client/sequencer-client.ts +24 -18
- package/src/config.ts +23 -6
- package/src/global_variable_builder/global_builder.ts +19 -17
- package/src/publisher/config.ts +29 -27
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher-factory.ts +16 -3
- package/src/publisher/sequencer-publisher-metrics.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +257 -183
- package/src/sequencer/block_builder.ts +23 -28
- package/src/sequencer/config.ts +1 -1
- package/src/sequencer/errors.ts +21 -0
- package/src/sequencer/metrics.ts +71 -98
- package/src/sequencer/sequencer.ts +546 -237
- package/src/sequencer/timetable.ts +10 -14
- package/src/sequencer/utils.ts +10 -24
- package/src/test/index.ts +1 -1
- package/src/tx_validator/tx_validator_factory.ts +13 -7
|
@@ -4,26 +4,35 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
4
4
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { getKzg } from '@aztec/blob-lib';
|
|
8
|
+
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
9
|
+
import { NoCommitteeError } from '@aztec/ethereum/contracts';
|
|
10
|
+
import { FormattedViemError } from '@aztec/ethereum/utils';
|
|
11
|
+
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
9
12
|
import { omit, pick } from '@aztec/foundation/collection';
|
|
13
|
+
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
14
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
10
15
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
11
|
-
import {
|
|
16
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
12
17
|
import { createLogger } from '@aztec/foundation/log';
|
|
13
18
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
14
19
|
import { Timer } from '@aztec/foundation/timer';
|
|
15
|
-
import {
|
|
20
|
+
import { unfreeze } from '@aztec/foundation/types';
|
|
21
|
+
import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
22
|
+
import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
16
23
|
import { Gas } from '@aztec/stdlib/gas';
|
|
17
24
|
import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
|
|
18
25
|
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
26
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
19
27
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
20
28
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
21
|
-
import { ContentCommitment
|
|
29
|
+
import { ContentCommitment } from '@aztec/stdlib/tx';
|
|
22
30
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
23
31
|
import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
24
32
|
import EventEmitter from 'node:events';
|
|
33
|
+
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
25
34
|
import { SequencerMetrics } from './metrics.js';
|
|
26
|
-
import { SequencerTimetable
|
|
35
|
+
import { SequencerTimetable } from './timetable.js';
|
|
27
36
|
import { SequencerState } from './utils.js';
|
|
28
37
|
export { SequencerState };
|
|
29
38
|
/**
|
|
@@ -62,6 +71,8 @@ export { SequencerState };
|
|
|
62
71
|
metrics;
|
|
63
72
|
lastBlockPublished;
|
|
64
73
|
governanceProposerPayload;
|
|
74
|
+
/** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
|
|
75
|
+
/** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */ lastSlotForValidationBlock;
|
|
65
76
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
66
77
|
enforceTimeTable;
|
|
67
78
|
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
@@ -72,10 +83,11 @@ export { SequencerState };
|
|
|
72
83
|
publisher;
|
|
73
84
|
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
74
85
|
super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.blockBuilder = blockBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.config = config, this.telemetry = telemetry, this.log = log, this.pollingIntervalMs = 1000, this.maxTxsPerBlock = 32, this.minTxsPerBlock = 1, this.maxL1TxInclusionTimeIntoSlot = 0, this.state = SequencerState.STOPPED, this.maxBlockSizeInBytes = 1024 * 1024, this.maxBlockGas = new Gas(100e9, 100e9), this.enforceTimeTable = false;
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
87
|
+
if (this.config.fishermanMode) {
|
|
88
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
89
|
+
}
|
|
90
|
+
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
79
91
|
// Initialize config
|
|
80
92
|
this.updateConfig(this.config);
|
|
81
93
|
}
|
|
@@ -135,13 +147,14 @@ export { SequencerState };
|
|
|
135
147
|
}, this.metrics, this.log);
|
|
136
148
|
}
|
|
137
149
|
async init() {
|
|
150
|
+
// Takes ~3s to precompute some tables.
|
|
151
|
+
getKzg();
|
|
138
152
|
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
139
153
|
}
|
|
140
154
|
/**
|
|
141
155
|
* Starts the sequencer and moves to IDLE state.
|
|
142
156
|
*/ start() {
|
|
143
|
-
this.
|
|
144
|
-
this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
|
|
157
|
+
this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
|
|
145
158
|
this.setState(SequencerState.IDLE, undefined, {
|
|
146
159
|
force: true
|
|
147
160
|
});
|
|
@@ -152,9 +165,10 @@ export { SequencerState };
|
|
|
152
165
|
* Stops the sequencer from processing txs and moves to STOPPED state.
|
|
153
166
|
*/ async stop() {
|
|
154
167
|
this.log.info(`Stopping sequencer`);
|
|
155
|
-
this.
|
|
168
|
+
this.setState(SequencerState.STOPPING, undefined, {
|
|
169
|
+
force: true
|
|
170
|
+
});
|
|
156
171
|
this.publisher?.interrupt();
|
|
157
|
-
await this.validatorClient?.stop();
|
|
158
172
|
await this.runningPromise?.stop();
|
|
159
173
|
this.setState(SequencerState.STOPPED, undefined, {
|
|
160
174
|
force: true
|
|
@@ -176,21 +190,28 @@ export { SequencerState };
|
|
|
176
190
|
* - Collect attestations for the block
|
|
177
191
|
* - Submit block
|
|
178
192
|
* - If our block for some reason is not included, revert the state
|
|
179
|
-
*/ async
|
|
193
|
+
*/ async work() {
|
|
180
194
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
196
|
+
// Check we have not already published a block for this slot (cheapest check)
|
|
197
|
+
if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
|
|
198
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
202
|
+
const syncedTo = await this.checkSync({
|
|
203
|
+
ts,
|
|
204
|
+
slot
|
|
205
|
+
});
|
|
184
206
|
if (!syncedTo) {
|
|
207
|
+
await this.tryVoteWhenSyncFails({
|
|
208
|
+
slot,
|
|
209
|
+
ts
|
|
210
|
+
});
|
|
185
211
|
return;
|
|
186
212
|
}
|
|
187
213
|
const chainTipArchive = syncedTo.archive;
|
|
188
|
-
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
189
|
-
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
190
|
-
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
191
|
-
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
192
|
-
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
193
|
-
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
214
|
+
const newBlockNumber = BlockNumber(syncedTo.blockNumber + 1);
|
|
194
215
|
const syncLogData = {
|
|
195
216
|
now,
|
|
196
217
|
syncedToL1Ts: syncedTo.l1Timestamp,
|
|
@@ -201,73 +222,66 @@ export { SequencerState };
|
|
|
201
222
|
newBlockNumber,
|
|
202
223
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
|
|
203
224
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
211
|
-
...syncLogData,
|
|
212
|
-
block: syncedTo.block.header.toInspect()
|
|
213
|
-
});
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
// Or that we haven't published it ourselves
|
|
217
|
-
if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
|
|
218
|
-
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`, {
|
|
219
|
-
...syncLogData,
|
|
220
|
-
block: this.lastBlockPublished.header.toInspect()
|
|
221
|
-
});
|
|
225
|
+
// Check that we are a proposer for the next slot
|
|
226
|
+
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
227
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
228
|
+
// If we are not a proposer check if we should invalidate a invalid block, and bail
|
|
229
|
+
if (!canPropose) {
|
|
230
|
+
await this.considerInvalidatingBlock(syncedTo, slot);
|
|
222
231
|
return;
|
|
223
232
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.log.warn(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`);
|
|
233
|
+
// In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
|
|
234
|
+
if (this.config.fishermanMode) {
|
|
235
|
+
if (this.lastSlotForValidationBlock === slot) {
|
|
236
|
+
this.log.trace(`Already validated block building for slot ${slot} (skipping)`, {
|
|
237
|
+
slot
|
|
238
|
+
});
|
|
231
239
|
return;
|
|
232
240
|
}
|
|
241
|
+
this.log.debug(`Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`, {
|
|
242
|
+
slot,
|
|
243
|
+
proposer: proposer?.toString()
|
|
244
|
+
});
|
|
245
|
+
// Mark this slot as being validated
|
|
246
|
+
this.lastSlotForValidationBlock = slot;
|
|
233
247
|
}
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
us: validatorAddresses,
|
|
240
|
-
proposer: proposerInNextSlot,
|
|
241
|
-
...syncLogData
|
|
248
|
+
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
249
|
+
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
250
|
+
this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
251
|
+
...syncLogData,
|
|
252
|
+
block: syncedTo.block.header.toInspect()
|
|
242
253
|
});
|
|
243
|
-
|
|
244
|
-
if (!syncedTo.pendingChainValidationStatus.valid) {
|
|
245
|
-
// We pass i undefined here to get any available publisher.
|
|
246
|
-
const { publisher } = await this.publisherFactory.create(undefined);
|
|
247
|
-
await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
|
|
248
|
-
}
|
|
254
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
|
|
249
255
|
return;
|
|
250
256
|
}
|
|
251
|
-
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
252
|
-
// if all the previous checks are good, but we do it just in case.
|
|
253
|
-
const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
|
|
254
257
|
// We now need to get ourselves a publisher.
|
|
255
258
|
// The returned attestor will be the one we provided if we provided one.
|
|
256
259
|
// Otherwise it will be a valid attestor for the returned publisher.
|
|
257
|
-
|
|
260
|
+
// In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
|
|
261
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(this.config.fishermanMode ? undefined : proposer);
|
|
258
262
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
259
263
|
this.publisher = publisher;
|
|
264
|
+
// In fisherman mode, set the actual proposer's address for simulations
|
|
265
|
+
if (this.config.fishermanMode) {
|
|
266
|
+
if (proposer) {
|
|
267
|
+
publisher.setProposerAddressForSimulation(proposer);
|
|
268
|
+
this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Get proposer credentials
|
|
260
272
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
|
|
261
273
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
|
|
262
|
-
this.metrics.setCoinbase(coinbase);
|
|
263
274
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
264
275
|
const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
|
|
265
|
-
|
|
276
|
+
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
277
|
+
// if all the previous checks are good, but we do it just in case.
|
|
278
|
+
const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposer ?? EthAddress.ZERO, invalidateBlock);
|
|
266
279
|
if (canProposeCheck === undefined) {
|
|
267
280
|
this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
|
|
268
281
|
this.emit('proposer-rollup-check-failed', {
|
|
269
282
|
reason: 'Rollup contract check failed'
|
|
270
283
|
});
|
|
284
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
271
285
|
return;
|
|
272
286
|
} else if (canProposeCheck.slot !== slot) {
|
|
273
287
|
this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${slot} but got ${canProposeCheck.slot}.`, {
|
|
@@ -279,9 +293,10 @@ export { SequencerState };
|
|
|
279
293
|
this.emit('proposer-rollup-check-failed', {
|
|
280
294
|
reason: 'Slot mismatch'
|
|
281
295
|
});
|
|
296
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
282
297
|
return;
|
|
283
|
-
} else if (canProposeCheck.
|
|
284
|
-
this.log.warn(`Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.
|
|
298
|
+
} else if (canProposeCheck.checkpointNumber !== CheckpointNumber.fromBlockNumber(newBlockNumber)) {
|
|
299
|
+
this.log.warn(`Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.checkpointNumber}.`, {
|
|
285
300
|
...syncLogData,
|
|
286
301
|
rollup: canProposeCheck,
|
|
287
302
|
newBlockNumber,
|
|
@@ -290,48 +305,78 @@ export { SequencerState };
|
|
|
290
305
|
this.emit('proposer-rollup-check-failed', {
|
|
291
306
|
reason: 'Block mismatch'
|
|
292
307
|
});
|
|
308
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
293
309
|
return;
|
|
294
310
|
}
|
|
295
|
-
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}
|
|
296
|
-
...syncLogData
|
|
297
|
-
validatorAddresses
|
|
311
|
+
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
|
|
312
|
+
...syncLogData
|
|
298
313
|
});
|
|
299
314
|
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
blockNumber: newBlockNumber,
|
|
305
|
-
slot
|
|
306
|
-
});
|
|
307
|
-
return false;
|
|
308
|
-
}) : Promise.resolve(false);
|
|
309
|
-
const enqueueSlashingActionsPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
|
|
310
|
-
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
311
|
-
blockNumber: newBlockNumber,
|
|
312
|
-
slot
|
|
313
|
-
});
|
|
314
|
-
return false;
|
|
315
|
-
}) : Promise.resolve(false);
|
|
315
|
+
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
316
|
+
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
317
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
|
|
318
|
+
// Enqueues block invalidation
|
|
316
319
|
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
317
320
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
318
321
|
}
|
|
322
|
+
// Actual block building
|
|
319
323
|
this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
|
|
324
|
+
this.metrics.incOpenSlot(slot, proposer?.toString() ?? 'unknown');
|
|
325
|
+
const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
|
|
326
|
+
// Wait until the voting promises have resolved, so all requests are enqueued
|
|
327
|
+
await Promise.all(votesPromises);
|
|
328
|
+
// In fisherman mode, we don't publish to L1
|
|
329
|
+
if (this.config.fishermanMode) {
|
|
330
|
+
// Clear pending requests
|
|
331
|
+
publisher.clearPendingRequests();
|
|
332
|
+
if (block) {
|
|
333
|
+
this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
|
|
334
|
+
blockNumber: newBlockNumber,
|
|
335
|
+
slot: Number(slot),
|
|
336
|
+
archive: block.archive.toString(),
|
|
337
|
+
txCount: block.body.txEffects.length
|
|
338
|
+
});
|
|
339
|
+
this.lastBlockPublished = block;
|
|
340
|
+
this.metrics.recordBlockProposalSuccess();
|
|
341
|
+
} else {
|
|
342
|
+
// Block building failed in fisherman mode
|
|
343
|
+
this.log.warn(`Validation block building FAILED for slot ${slot}`, {
|
|
344
|
+
blockNumber: newBlockNumber,
|
|
345
|
+
slot: Number(slot)
|
|
346
|
+
});
|
|
347
|
+
this.metrics.recordBlockProposalFailed('block_build_failed');
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// Normal mode: send the tx to L1
|
|
351
|
+
const l1Response = await publisher.sendRequests();
|
|
352
|
+
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
353
|
+
if (proposedBlock) {
|
|
354
|
+
this.lastBlockPublished = block;
|
|
355
|
+
this.emit('block-published', {
|
|
356
|
+
blockNumber: newBlockNumber,
|
|
357
|
+
slot: Number(slot)
|
|
358
|
+
});
|
|
359
|
+
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
360
|
+
} else if (block) {
|
|
361
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
365
|
+
}
|
|
366
|
+
/** Tries building a block proposal, and if successful, enqueues it for publishing. */ async tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock) {
|
|
320
367
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
321
|
-
proposer
|
|
322
|
-
coinbase,
|
|
368
|
+
proposer,
|
|
323
369
|
publisher: publisher.getSenderAddress(),
|
|
324
|
-
feeRecipient,
|
|
325
370
|
globalVariables: newGlobalVariables.toInspect(),
|
|
326
371
|
chainTipArchive,
|
|
327
372
|
blockNumber: newBlockNumber,
|
|
328
373
|
slot
|
|
329
374
|
});
|
|
330
|
-
|
|
331
|
-
const proposalHeader = ProposedBlockHeader.from({
|
|
375
|
+
const proposalHeader = CheckpointHeader.from({
|
|
332
376
|
...newGlobalVariables,
|
|
333
377
|
timestamp: newGlobalVariables.timestamp,
|
|
334
378
|
lastArchiveRoot: chainTipArchive,
|
|
379
|
+
blockHeadersHash: Fr.ZERO,
|
|
335
380
|
contentCommitment: ContentCommitment.empty(),
|
|
336
381
|
totalManaUsed: Fr.ZERO
|
|
337
382
|
});
|
|
@@ -342,7 +387,7 @@ export { SequencerState };
|
|
|
342
387
|
// and also we may need to fetch more if we don't have enough valid txs.
|
|
343
388
|
const pendingTxs = this.p2pClient.iteratePendingTxs();
|
|
344
389
|
try {
|
|
345
|
-
block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables,
|
|
390
|
+
block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
|
|
346
391
|
} catch (err) {
|
|
347
392
|
this.emit('block-build-failed', {
|
|
348
393
|
reason: err.message
|
|
@@ -355,6 +400,7 @@ export { SequencerState };
|
|
|
355
400
|
slot
|
|
356
401
|
});
|
|
357
402
|
}
|
|
403
|
+
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
358
404
|
}
|
|
359
405
|
} else {
|
|
360
406
|
this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
|
|
@@ -366,28 +412,13 @@ export { SequencerState };
|
|
|
366
412
|
minTxs: this.minTxsPerBlock,
|
|
367
413
|
availableTxs: pendingTxCount
|
|
368
414
|
});
|
|
415
|
+
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
369
416
|
}
|
|
370
|
-
|
|
371
|
-
enqueueGovernanceSignalPromise,
|
|
372
|
-
enqueueSlashingActionsPromise
|
|
373
|
-
]);
|
|
374
|
-
const l1Response = await publisher.sendRequests();
|
|
375
|
-
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
376
|
-
if (proposedBlock) {
|
|
377
|
-
this.lastBlockPublished = block;
|
|
378
|
-
this.emit('block-published', {
|
|
379
|
-
blockNumber: newBlockNumber,
|
|
380
|
-
slot: Number(slot)
|
|
381
|
-
});
|
|
382
|
-
this.metrics.incFilledSlot(publisher.getSenderAddress().toString());
|
|
383
|
-
} else if (block) {
|
|
384
|
-
this.emit('block-publish-failed', l1Response ?? {});
|
|
385
|
-
}
|
|
386
|
-
this.setState(SequencerState.IDLE, undefined);
|
|
417
|
+
return block;
|
|
387
418
|
}
|
|
388
|
-
async
|
|
419
|
+
async safeWork() {
|
|
389
420
|
try {
|
|
390
|
-
await this.
|
|
421
|
+
await this.work();
|
|
391
422
|
} catch (err) {
|
|
392
423
|
if (err instanceof SequencerTooSlowError) {
|
|
393
424
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
@@ -407,6 +438,10 @@ export { SequencerState };
|
|
|
407
438
|
}
|
|
408
439
|
}
|
|
409
440
|
setState(proposedState, slotNumber, opts = {}) {
|
|
441
|
+
if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
|
|
442
|
+
this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
|
|
443
|
+
throw new SequencerInterruptedError();
|
|
444
|
+
}
|
|
410
445
|
if (this.state === SequencerState.STOPPED && !opts.force) {
|
|
411
446
|
this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
|
|
412
447
|
return;
|
|
@@ -416,7 +451,12 @@ export { SequencerState };
|
|
|
416
451
|
secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
|
|
417
452
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
418
453
|
}
|
|
419
|
-
|
|
454
|
+
const boringStates = [
|
|
455
|
+
SequencerState.IDLE,
|
|
456
|
+
SequencerState.SYNCHRONIZING
|
|
457
|
+
];
|
|
458
|
+
const logLevel = boringStates.includes(proposedState) && boringStates.includes(this.state) ? 'trace' : 'debug';
|
|
459
|
+
this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, {
|
|
420
460
|
slotNumber,
|
|
421
461
|
secondsIntoSlot
|
|
422
462
|
});
|
|
@@ -447,6 +487,7 @@ export { SequencerState };
|
|
|
447
487
|
maxTransactions: this.maxTxsPerBlock,
|
|
448
488
|
maxBlockSize: this.maxBlockSizeInBytes,
|
|
449
489
|
maxBlockGas: this.maxBlockGas,
|
|
490
|
+
maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
|
|
450
491
|
deadline
|
|
451
492
|
};
|
|
452
493
|
}
|
|
@@ -463,13 +504,13 @@ export { SequencerState };
|
|
|
463
504
|
*/ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock, publisher) {
|
|
464
505
|
await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
465
506
|
const blockNumber = newGlobalVariables.blockNumber;
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
|
|
507
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
|
|
508
|
+
const slot = proposalHeader.slotNumber;
|
|
509
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
469
510
|
const workTimer = new Timer();
|
|
470
511
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
471
512
|
try {
|
|
472
|
-
const blockBuilderOptions = this.getBlockBuilderOptions(
|
|
513
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(slot);
|
|
473
514
|
const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
|
|
474
515
|
const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
|
|
475
516
|
const blockBuildDuration = workTimer.ms();
|
|
@@ -485,7 +526,7 @@ export { SequencerState };
|
|
|
485
526
|
}
|
|
486
527
|
// TODO(@PhilWindle) We should probably periodically check for things like another
|
|
487
528
|
// block being published before ours instead of just waiting on our block
|
|
488
|
-
await publisher.validateBlockHeader(block.
|
|
529
|
+
await publisher.validateBlockHeader(block.getCheckpointHeader(), invalidateBlock);
|
|
489
530
|
const blockStats = {
|
|
490
531
|
eventName: 'l2-block-built',
|
|
491
532
|
creator: proposerAddress?.toString() ?? publisher.getSenderAddress().toString(),
|
|
@@ -502,15 +543,23 @@ export { SequencerState };
|
|
|
502
543
|
txHashes,
|
|
503
544
|
...blockStats
|
|
504
545
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
this.log.
|
|
546
|
+
// In fisherman mode, skip attestation collection
|
|
547
|
+
let attestationsAndSigners;
|
|
548
|
+
if (this.config.fishermanMode) {
|
|
549
|
+
this.log.debug('Skipping attestation collection');
|
|
550
|
+
attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
|
|
551
|
+
} else {
|
|
552
|
+
this.log.debug('Collecting attestations');
|
|
553
|
+
attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
|
|
554
|
+
this.log.verbose(`Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`, {
|
|
509
555
|
blockHash,
|
|
510
|
-
blockNumber
|
|
556
|
+
blockNumber,
|
|
557
|
+
slot
|
|
511
558
|
});
|
|
512
559
|
}
|
|
513
|
-
|
|
560
|
+
// In fisherman mode, skip attestation signing
|
|
561
|
+
const attestationsAndSignersSignature = this.config.fishermanMode || !this.validatorClient ? Signature.empty() : await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress());
|
|
562
|
+
await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
|
|
514
563
|
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
515
564
|
return block;
|
|
516
565
|
} catch (err) {
|
|
@@ -519,69 +568,110 @@ export { SequencerState };
|
|
|
519
568
|
}
|
|
520
569
|
}
|
|
521
570
|
async collectAttestations(block, txs, proposerAddress) {
|
|
522
|
-
const { committee } = await this.epochCache.getCommittee(block.
|
|
571
|
+
const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
|
|
523
572
|
// We checked above that the committee is defined, so this should never happen.
|
|
524
573
|
if (!committee) {
|
|
525
574
|
throw new Error('No committee when collecting attestations');
|
|
526
575
|
}
|
|
527
576
|
if (committee.length === 0) {
|
|
528
577
|
this.log.verbose(`Attesting committee is empty`);
|
|
529
|
-
return
|
|
578
|
+
return CommitteeAttestationsAndSigners.empty();
|
|
530
579
|
} else {
|
|
531
580
|
this.log.debug(`Attesting committee length is ${committee.length}`);
|
|
532
581
|
}
|
|
533
582
|
if (!this.validatorClient) {
|
|
534
|
-
|
|
535
|
-
this.log.error(msg);
|
|
536
|
-
throw new Error(msg);
|
|
583
|
+
throw new Error('Missing validator client: Cannot collect attestations');
|
|
537
584
|
}
|
|
538
585
|
const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
|
|
539
|
-
const slotNumber = block.header.globalVariables.slotNumber
|
|
586
|
+
const slotNumber = block.header.globalVariables.slotNumber;
|
|
540
587
|
this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
|
|
541
588
|
this.log.debug('Creating block proposal for validators');
|
|
542
589
|
const blockProposalOptions = {
|
|
543
|
-
publishFullTxs: !!this.config.publishTxsWithProposals
|
|
590
|
+
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
591
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
544
592
|
};
|
|
545
|
-
const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.
|
|
593
|
+
const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), block.archive.root, txs, proposerAddress, blockProposalOptions);
|
|
546
594
|
if (!proposal) {
|
|
547
595
|
throw new Error(`Failed to create block proposal`);
|
|
548
596
|
}
|
|
549
597
|
if (this.config.skipCollectingAttestations) {
|
|
550
598
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
551
599
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
552
|
-
return orderAttestations(attestations ?? [], committee);
|
|
600
|
+
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
553
601
|
}
|
|
554
602
|
this.log.debug('Broadcasting block proposal to validators');
|
|
555
603
|
await this.validatorClient.broadcastBlockProposal(proposal);
|
|
556
604
|
const attestationTimeAllowed = this.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_BLOCK) : this.aztecSlotDuration;
|
|
557
605
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
558
606
|
const timer = new Timer();
|
|
559
|
-
let
|
|
607
|
+
let collectedAttestationsCount = 0;
|
|
560
608
|
try {
|
|
561
609
|
const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
|
|
562
610
|
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
563
|
-
|
|
611
|
+
collectedAttestationsCount = attestations.length;
|
|
564
612
|
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
565
|
-
|
|
613
|
+
const sorted = orderAttestations(attestations, committee);
|
|
614
|
+
// manipulate the attestations if we've been configured to do so
|
|
615
|
+
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
616
|
+
return this.manipulateAttestations(block, epoch, seed, committee, sorted);
|
|
617
|
+
}
|
|
618
|
+
return new CommitteeAttestationsAndSigners(sorted);
|
|
566
619
|
} catch (err) {
|
|
567
620
|
if (err && err instanceof AttestationTimeoutError) {
|
|
568
|
-
|
|
621
|
+
collectedAttestationsCount = err.collectedCount;
|
|
569
622
|
}
|
|
570
623
|
throw err;
|
|
571
624
|
} finally{
|
|
572
|
-
this.metrics.recordCollectedAttestations(
|
|
625
|
+
this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
|
|
573
626
|
}
|
|
574
627
|
}
|
|
628
|
+
/** Breaks the attestations before publishing based on attack configs */ manipulateAttestations(block, epoch, seed, committee, attestations) {
|
|
629
|
+
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
630
|
+
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
631
|
+
const proposerIndex = Number(this.epochCache.computeProposerIndex(block.slot, epoch, seed, BigInt(committee.length)));
|
|
632
|
+
if (this.config.injectFakeAttestation) {
|
|
633
|
+
// Find non-empty attestations that are not from the proposer
|
|
634
|
+
const nonProposerIndices = [];
|
|
635
|
+
for(let i = 0; i < attestations.length; i++){
|
|
636
|
+
if (!attestations[i].signature.isEmpty() && i !== proposerIndex) {
|
|
637
|
+
nonProposerIndices.push(i);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (nonProposerIndices.length > 0) {
|
|
641
|
+
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
642
|
+
this.log.warn(`Injecting fake attestation in block ${block.number} at index ${targetIndex}`);
|
|
643
|
+
unfreeze(attestations[targetIndex]).signature = Signature.random();
|
|
644
|
+
}
|
|
645
|
+
return new CommitteeAttestationsAndSigners(attestations);
|
|
646
|
+
}
|
|
647
|
+
if (this.config.shuffleAttestationOrdering) {
|
|
648
|
+
this.log.warn(`Shuffling attestation ordering in block ${block.number} (proposer index ${proposerIndex})`);
|
|
649
|
+
const shuffled = [
|
|
650
|
+
...attestations
|
|
651
|
+
];
|
|
652
|
+
const [i, j] = [
|
|
653
|
+
(proposerIndex + 1) % shuffled.length,
|
|
654
|
+
(proposerIndex + 2) % shuffled.length
|
|
655
|
+
];
|
|
656
|
+
const valueI = shuffled[i];
|
|
657
|
+
const valueJ = shuffled[j];
|
|
658
|
+
shuffled[i] = valueJ;
|
|
659
|
+
shuffled[j] = valueI;
|
|
660
|
+
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
661
|
+
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
662
|
+
}
|
|
663
|
+
return new CommitteeAttestationsAndSigners(attestations);
|
|
664
|
+
}
|
|
575
665
|
/**
|
|
576
666
|
* Publishes the L2Block to the rollup contract.
|
|
577
667
|
* @param block - The L2Block to be published.
|
|
578
|
-
*/ async enqueuePublishL2Block(block,
|
|
668
|
+
*/ async enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher) {
|
|
579
669
|
// Publishes new block to the network and awaits the tx to be mined
|
|
580
|
-
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber
|
|
670
|
+
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
|
|
581
671
|
// Time out tx at the end of the slot
|
|
582
|
-
const slot = block.header.globalVariables.slotNumber
|
|
672
|
+
const slot = block.header.globalVariables.slotNumber;
|
|
583
673
|
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
584
|
-
const enqueued = await publisher.enqueueProposeL2Block(block,
|
|
674
|
+
const enqueued = await publisher.enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, {
|
|
585
675
|
txTimeoutAt,
|
|
586
676
|
forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber
|
|
587
677
|
});
|
|
@@ -592,8 +682,20 @@ export { SequencerState };
|
|
|
592
682
|
/**
|
|
593
683
|
* Returns whether all dependencies have caught up.
|
|
594
684
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
595
|
-
|
|
596
|
-
|
|
685
|
+
*/ async checkSync(args) {
|
|
686
|
+
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
687
|
+
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
688
|
+
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
689
|
+
const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
|
|
690
|
+
const { slot, ts } = args;
|
|
691
|
+
if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
|
|
692
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
693
|
+
slot,
|
|
694
|
+
ts,
|
|
695
|
+
l1Timestamp
|
|
696
|
+
});
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
597
699
|
const syncedBlocks = await Promise.all([
|
|
598
700
|
this.worldState.status().then(({ syncSummary })=>({
|
|
599
701
|
number: syncSummary.latestBlockNumber,
|
|
@@ -602,66 +704,218 @@ export { SequencerState };
|
|
|
602
704
|
this.l2BlockSource.getL2Tips().then((t)=>t.latest),
|
|
603
705
|
this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
|
|
604
706
|
this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
|
|
605
|
-
this.l2BlockSource.getL1Timestamp(),
|
|
606
707
|
this.l2BlockSource.getPendingChainValidationStatus()
|
|
607
708
|
]);
|
|
608
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource,
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
const result = l2BlockSource.
|
|
612
|
-
const logData = {
|
|
613
|
-
worldState,
|
|
614
|
-
l2BlockSource,
|
|
615
|
-
p2p,
|
|
616
|
-
l1ToL2MessageSource
|
|
617
|
-
};
|
|
618
|
-
this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
|
|
709
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
|
|
710
|
+
// Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
|
|
711
|
+
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
712
|
+
const result = l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 || worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
|
|
619
713
|
if (!result) {
|
|
714
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
715
|
+
worldState,
|
|
716
|
+
l2BlockSource,
|
|
717
|
+
p2p,
|
|
718
|
+
l1ToL2MessageSource
|
|
719
|
+
});
|
|
620
720
|
return undefined;
|
|
621
721
|
}
|
|
722
|
+
// Special case for genesis state
|
|
622
723
|
const blockNumber = worldState.number;
|
|
623
|
-
if (blockNumber
|
|
624
|
-
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
625
|
-
if (!block) {
|
|
626
|
-
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
627
|
-
this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
|
|
628
|
-
return undefined;
|
|
629
|
-
}
|
|
630
|
-
return {
|
|
631
|
-
block,
|
|
632
|
-
blockNumber: block.number,
|
|
633
|
-
archive: block.archive.root,
|
|
634
|
-
l1Timestamp,
|
|
635
|
-
pendingChainValidationStatus
|
|
636
|
-
};
|
|
637
|
-
} else {
|
|
724
|
+
if (blockNumber < INITIAL_L2_BLOCK_NUM) {
|
|
638
725
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
639
726
|
return {
|
|
640
|
-
blockNumber: INITIAL_L2_BLOCK_NUM - 1,
|
|
727
|
+
blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1),
|
|
641
728
|
archive,
|
|
642
729
|
l1Timestamp,
|
|
643
730
|
pendingChainValidationStatus
|
|
644
731
|
};
|
|
645
732
|
}
|
|
733
|
+
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
734
|
+
if (!block) {
|
|
735
|
+
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
736
|
+
this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
block,
|
|
741
|
+
blockNumber: block.number,
|
|
742
|
+
archive: block.archive.root,
|
|
743
|
+
l1Timestamp,
|
|
744
|
+
pendingChainValidationStatus
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Enqueues governance and slashing votes with the publisher. Does not block.
|
|
749
|
+
* @param publisher - The publisher to enqueue votes with
|
|
750
|
+
* @param attestorAddress - The attestor address to use for signing
|
|
751
|
+
* @param slot - The slot number
|
|
752
|
+
* @param timestamp - The timestamp for the votes
|
|
753
|
+
* @param context - Optional context for logging (e.g., block number)
|
|
754
|
+
* @returns A tuple of [governanceEnqueued, slashingEnqueued]
|
|
755
|
+
*/ enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, timestamp) {
|
|
756
|
+
try {
|
|
757
|
+
const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
|
|
758
|
+
const enqueueGovernancePromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
|
|
759
|
+
this.log.error(`Error enqueuing governance vote`, err, {
|
|
760
|
+
slot
|
|
761
|
+
});
|
|
762
|
+
return false;
|
|
763
|
+
}) : undefined;
|
|
764
|
+
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>{
|
|
765
|
+
// Record metrics for fisherman mode
|
|
766
|
+
if (this.config.fishermanMode && actions.length > 0) {
|
|
767
|
+
this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
|
|
768
|
+
slot,
|
|
769
|
+
actionCount: actions.length
|
|
770
|
+
});
|
|
771
|
+
this.metrics.recordSlashingAttempt(actions.length);
|
|
772
|
+
}
|
|
773
|
+
// Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
|
|
774
|
+
return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
|
|
775
|
+
}).catch((err)=>{
|
|
776
|
+
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
777
|
+
slot
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}) : undefined;
|
|
781
|
+
return [
|
|
782
|
+
enqueueGovernancePromise,
|
|
783
|
+
enqueueSlashingPromise
|
|
784
|
+
];
|
|
785
|
+
} catch (err) {
|
|
786
|
+
this.log.error(`Error enqueueing governance and slashing votes`, err);
|
|
787
|
+
return [
|
|
788
|
+
undefined,
|
|
789
|
+
undefined
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Checks if we are the proposer for the next slot.
|
|
795
|
+
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
796
|
+
*/ async checkCanPropose(slot) {
|
|
797
|
+
let proposer;
|
|
798
|
+
try {
|
|
799
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
|
|
800
|
+
} catch (e) {
|
|
801
|
+
if (e instanceof NoCommitteeError) {
|
|
802
|
+
this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
|
|
803
|
+
return [
|
|
804
|
+
false,
|
|
805
|
+
undefined
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
this.log.error(`Error getting proposer for slot ${slot}`, e);
|
|
809
|
+
return [
|
|
810
|
+
false,
|
|
811
|
+
undefined
|
|
812
|
+
];
|
|
813
|
+
}
|
|
814
|
+
// If proposer is undefined, then the committee is empty and anyone may propose
|
|
815
|
+
if (proposer === undefined) {
|
|
816
|
+
return [
|
|
817
|
+
true,
|
|
818
|
+
undefined
|
|
819
|
+
];
|
|
820
|
+
}
|
|
821
|
+
// In fisherman mode, just return the current proposer
|
|
822
|
+
if (this.config.fishermanMode) {
|
|
823
|
+
return [
|
|
824
|
+
true,
|
|
825
|
+
proposer
|
|
826
|
+
];
|
|
827
|
+
}
|
|
828
|
+
const validatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
829
|
+
const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
|
|
830
|
+
if (!weAreProposer) {
|
|
831
|
+
this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
|
|
832
|
+
validatorAddresses,
|
|
833
|
+
proposer
|
|
834
|
+
});
|
|
835
|
+
return [
|
|
836
|
+
false,
|
|
837
|
+
proposer
|
|
838
|
+
];
|
|
839
|
+
}
|
|
840
|
+
return [
|
|
841
|
+
true,
|
|
842
|
+
proposer
|
|
843
|
+
];
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
|
|
847
|
+
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
848
|
+
*/ async tryVoteWhenSyncFails(args) {
|
|
849
|
+
const { slot, ts } = args;
|
|
850
|
+
// Prevent duplicate attempts in the same slot
|
|
851
|
+
if (this.lastSlotForVoteWhenSyncFailed === slot) {
|
|
852
|
+
this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
// Check if we're past the max time for initializing a proposal
|
|
856
|
+
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
857
|
+
const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
|
|
858
|
+
// If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
|
|
859
|
+
// We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
|
|
860
|
+
if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
|
|
861
|
+
this.log.trace(`Not attempting to vote since there is still for block building`, {
|
|
862
|
+
secondsIntoSlot,
|
|
863
|
+
maxAllowedTime
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
|
|
868
|
+
secondsIntoSlot,
|
|
869
|
+
maxAllowedTime
|
|
870
|
+
});
|
|
871
|
+
// Check if we're a proposer or proposal is open
|
|
872
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
873
|
+
if (!canPropose) {
|
|
874
|
+
this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, {
|
|
875
|
+
slot,
|
|
876
|
+
proposer
|
|
877
|
+
});
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
// Mark this slot as attempted
|
|
881
|
+
this.lastSlotForVoteWhenSyncFailed = slot;
|
|
882
|
+
// Get a publisher for voting
|
|
883
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
884
|
+
this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
|
|
885
|
+
attestorAddress,
|
|
886
|
+
slot
|
|
887
|
+
});
|
|
888
|
+
// Enqueue governance and slashing votes using the shared helper method
|
|
889
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
|
|
890
|
+
await Promise.all(votesPromises);
|
|
891
|
+
if (votesPromises.every((p)=>!p)) {
|
|
892
|
+
this.log.debug(`No votes to enqueue for slot ${slot}`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
this.log.info(`Voting in slot ${slot} despite sync failure`, {
|
|
896
|
+
slot
|
|
897
|
+
});
|
|
898
|
+
await publisher.sendRequests();
|
|
646
899
|
}
|
|
647
900
|
/**
|
|
648
901
|
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|
|
649
902
|
* has been there without being invalidated and whether the sequencer is in the committee or not. We always
|
|
650
903
|
* have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
|
|
651
904
|
* and if they fail, any sequencer will try as well.
|
|
652
|
-
*/ async considerInvalidatingBlock(syncedTo, currentSlot
|
|
905
|
+
*/ async considerInvalidatingBlock(syncedTo, currentSlot) {
|
|
653
906
|
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
654
907
|
if (pendingChainValidationStatus.valid) {
|
|
655
908
|
return;
|
|
656
909
|
}
|
|
657
|
-
const
|
|
658
|
-
const
|
|
659
|
-
const
|
|
910
|
+
const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
|
|
911
|
+
const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
|
|
912
|
+
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
|
|
913
|
+
const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
660
914
|
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
|
|
661
915
|
const logData = {
|
|
662
|
-
invalidL1Timestamp,
|
|
916
|
+
invalidL1Timestamp: invalidBlockTimestamp,
|
|
663
917
|
l1Timestamp,
|
|
664
|
-
invalidBlock: pendingChainValidationStatus.block
|
|
918
|
+
invalidBlock: pendingChainValidationStatus.block,
|
|
665
919
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
666
920
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
667
921
|
ourValidatorAddresses,
|
|
@@ -674,6 +928,21 @@ export { SequencerState };
|
|
|
674
928
|
this.log.debug(`Not invalidating pending chain`, logData);
|
|
675
929
|
return;
|
|
676
930
|
}
|
|
931
|
+
let validatorToUse;
|
|
932
|
+
if (invalidateAsCommitteeMember) {
|
|
933
|
+
// When invalidating as a committee member, use first validator that's actually in the committee
|
|
934
|
+
const { committee } = await this.epochCache.getCommittee(currentSlot);
|
|
935
|
+
if (committee) {
|
|
936
|
+
const committeeSet = new Set(committee.map((addr)=>addr.toString()));
|
|
937
|
+
validatorToUse = ourValidatorAddresses.find((addr)=>committeeSet.has(addr.toString())) ?? ourValidatorAddresses[0];
|
|
938
|
+
} else {
|
|
939
|
+
validatorToUse = ourValidatorAddresses[0];
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
// When invalidating as a non-committee member, use the first validator
|
|
943
|
+
validatorToUse = ourValidatorAddresses[0];
|
|
944
|
+
}
|
|
945
|
+
const { publisher } = await this.publisherFactory.create(validatorToUse);
|
|
677
946
|
const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
678
947
|
if (!invalidateBlock) {
|
|
679
948
|
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
@@ -681,10 +950,15 @@ export { SequencerState };
|
|
|
681
950
|
}
|
|
682
951
|
this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
|
|
683
952
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
684
|
-
|
|
953
|
+
if (!this.config.fishermanMode) {
|
|
954
|
+
await publisher.sendRequests();
|
|
955
|
+
} else {
|
|
956
|
+
this.log.info('Invalidating block in fisherman mode, clearing pending requests');
|
|
957
|
+
publisher.clearPendingRequests();
|
|
958
|
+
}
|
|
685
959
|
}
|
|
686
960
|
getSlotStartBuildTimestamp(slotNumber) {
|
|
687
|
-
return
|
|
961
|
+
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
|
688
962
|
}
|
|
689
963
|
getSecondsIntoSlot(slotNumber) {
|
|
690
964
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
|
|
@@ -702,7 +976,7 @@ export { SequencerState };
|
|
|
702
976
|
}
|
|
703
977
|
_ts_decorate([
|
|
704
978
|
trackSpan('Sequencer.work')
|
|
705
|
-
], Sequencer.prototype, "
|
|
979
|
+
], Sequencer.prototype, "safeWork", null);
|
|
706
980
|
_ts_decorate([
|
|
707
981
|
trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
|
|
708
982
|
[Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber
|