@aztec/sequencer-client 3.0.0-devnet.6 → 3.0.0-devnet.6-patch.1
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 +2 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +6 -2
- package/dest/config.d.ts +3 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -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 +7 -2
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +12 -1
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -2
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- 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.d.ts +40 -31
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +117 -62
- package/dest/sequencer/block_builder.d.ts +4 -3
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +5 -8
- package/dest/sequencer/config.d.ts +2 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +1 -1
- package/dest/sequencer/errors.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -1
- package/dest/sequencer/metrics.d.ts +12 -3
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +38 -0
- package/dest/sequencer/sequencer.d.ts +48 -27
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +418 -166
- package/dest/sequencer/timetable.d.ts +3 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/utils.d.ts +1 -1
- 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 +1 -1
- package/package.json +31 -30
- package/src/client/sequencer-client.ts +6 -9
- package/src/config.ts +12 -6
- package/src/global_variable_builder/global_builder.ts +19 -17
- package/src/publisher/config.ts +17 -6
- package/src/publisher/sequencer-publisher-factory.ts +4 -2
- package/src/publisher/sequencer-publisher.ts +165 -94
- package/src/sequencer/block_builder.ts +8 -12
- package/src/sequencer/config.ts +1 -1
- package/src/sequencer/metrics.ts +52 -3
- package/src/sequencer/sequencer.ts +480 -198
- package/src/sequencer/timetable.ts +7 -0
- package/src/test/index.ts +1 -1
- package/src/tx_validator/tx_validator_factory.ts +3 -2
|
@@ -4,18 +4,21 @@ 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';
|
|
10
|
-
import { randomInt } from '@aztec/foundation/crypto';
|
|
13
|
+
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
14
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
11
15
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
12
16
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
13
|
-
import { Fr } from '@aztec/foundation/fields';
|
|
14
17
|
import { createLogger } from '@aztec/foundation/log';
|
|
15
18
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
16
19
|
import { Timer } from '@aztec/foundation/timer';
|
|
17
20
|
import { unfreeze } from '@aztec/foundation/types';
|
|
18
|
-
import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
21
|
+
import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
19
22
|
import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
20
23
|
import { Gas } from '@aztec/stdlib/gas';
|
|
21
24
|
import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
|
|
@@ -68,6 +71,8 @@ export { SequencerState };
|
|
|
68
71
|
metrics;
|
|
69
72
|
lastBlockPublished;
|
|
70
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;
|
|
71
76
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
72
77
|
enforceTimeTable;
|
|
73
78
|
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
@@ -78,6 +83,10 @@ export { SequencerState };
|
|
|
78
83
|
publisher;
|
|
79
84
|
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
80
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;
|
|
86
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
87
|
+
if (this.config.fishermanMode) {
|
|
88
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
89
|
+
}
|
|
81
90
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
82
91
|
// Initialize config
|
|
83
92
|
this.updateConfig(this.config);
|
|
@@ -138,12 +147,14 @@ export { SequencerState };
|
|
|
138
147
|
}, this.metrics, this.log);
|
|
139
148
|
}
|
|
140
149
|
async init() {
|
|
150
|
+
// Takes ~3s to precompute some tables.
|
|
151
|
+
getKzg();
|
|
141
152
|
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
142
153
|
}
|
|
143
154
|
/**
|
|
144
155
|
* Starts the sequencer and moves to IDLE state.
|
|
145
156
|
*/ start() {
|
|
146
|
-
this.runningPromise = new RunningPromise(this.
|
|
157
|
+
this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
|
|
147
158
|
this.setState(SequencerState.IDLE, undefined, {
|
|
148
159
|
force: true
|
|
149
160
|
});
|
|
@@ -179,21 +190,28 @@ export { SequencerState };
|
|
|
179
190
|
* - Collect attestations for the block
|
|
180
191
|
* - Submit block
|
|
181
192
|
* - If our block for some reason is not included, revert the state
|
|
182
|
-
*/ async
|
|
193
|
+
*/ async work() {
|
|
183
194
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
});
|
|
187
206
|
if (!syncedTo) {
|
|
207
|
+
await this.tryVoteWhenSyncFails({
|
|
208
|
+
slot,
|
|
209
|
+
ts
|
|
210
|
+
});
|
|
188
211
|
return;
|
|
189
212
|
}
|
|
190
213
|
const chainTipArchive = syncedTo.archive;
|
|
191
|
-
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
192
|
-
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
193
|
-
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
194
|
-
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
195
|
-
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
196
|
-
// 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);
|
|
197
215
|
const syncLogData = {
|
|
198
216
|
now,
|
|
199
217
|
syncedToL1Ts: syncedTo.l1Timestamp,
|
|
@@ -204,72 +222,66 @@ export { SequencerState };
|
|
|
204
222
|
newBlockNumber,
|
|
205
223
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
|
|
206
224
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
214
|
-
...syncLogData,
|
|
215
|
-
block: syncedTo.block.header.toInspect()
|
|
216
|
-
});
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Or that we haven't published it ourselves
|
|
220
|
-
if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
|
|
221
|
-
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`, {
|
|
222
|
-
...syncLogData,
|
|
223
|
-
block: this.lastBlockPublished.header.toInspect()
|
|
224
|
-
});
|
|
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);
|
|
225
231
|
return;
|
|
226
232
|
}
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
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
|
+
});
|
|
234
239
|
return;
|
|
235
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;
|
|
236
247
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
us: validatorAddresses,
|
|
243
|
-
proposer: proposerInNextSlot,
|
|
244
|
-
...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()
|
|
245
253
|
});
|
|
246
|
-
|
|
247
|
-
if (!syncedTo.pendingChainValidationStatus.valid) {
|
|
248
|
-
// We pass i undefined here to get any available publisher.
|
|
249
|
-
const { publisher } = await this.publisherFactory.create(undefined);
|
|
250
|
-
await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
|
|
251
|
-
}
|
|
254
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
|
|
252
255
|
return;
|
|
253
256
|
}
|
|
254
|
-
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
255
|
-
// if all the previous checks are good, but we do it just in case.
|
|
256
|
-
const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
|
|
257
257
|
// We now need to get ourselves a publisher.
|
|
258
258
|
// The returned attestor will be the one we provided if we provided one.
|
|
259
259
|
// Otherwise it will be a valid attestor for the returned publisher.
|
|
260
|
-
|
|
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);
|
|
261
262
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
262
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
|
|
263
272
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
|
|
264
273
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
|
|
265
274
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
266
275
|
const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
|
|
267
|
-
|
|
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);
|
|
268
279
|
if (canProposeCheck === undefined) {
|
|
269
280
|
this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
|
|
270
281
|
this.emit('proposer-rollup-check-failed', {
|
|
271
282
|
reason: 'Rollup contract check failed'
|
|
272
283
|
});
|
|
284
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
273
285
|
return;
|
|
274
286
|
} else if (canProposeCheck.slot !== slot) {
|
|
275
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}.`, {
|
|
@@ -281,9 +293,10 @@ export { SequencerState };
|
|
|
281
293
|
this.emit('proposer-rollup-check-failed', {
|
|
282
294
|
reason: 'Slot mismatch'
|
|
283
295
|
});
|
|
296
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
284
297
|
return;
|
|
285
|
-
} else if (canProposeCheck.
|
|
286
|
-
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}.`, {
|
|
287
300
|
...syncLogData,
|
|
288
301
|
rollup: canProposeCheck,
|
|
289
302
|
newBlockNumber,
|
|
@@ -292,49 +305,78 @@ export { SequencerState };
|
|
|
292
305
|
this.emit('proposer-rollup-check-failed', {
|
|
293
306
|
reason: 'Block mismatch'
|
|
294
307
|
});
|
|
308
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
295
309
|
return;
|
|
296
310
|
}
|
|
297
|
-
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}
|
|
298
|
-
...syncLogData
|
|
299
|
-
validatorAddresses
|
|
311
|
+
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
|
|
312
|
+
...syncLogData
|
|
300
313
|
});
|
|
301
314
|
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
blockNumber: newBlockNumber,
|
|
307
|
-
slot
|
|
308
|
-
});
|
|
309
|
-
return false;
|
|
310
|
-
}) : Promise.resolve(false);
|
|
311
|
-
const enqueueSlashingActionsPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
|
|
312
|
-
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
313
|
-
blockNumber: newBlockNumber,
|
|
314
|
-
slot
|
|
315
|
-
});
|
|
316
|
-
return false;
|
|
317
|
-
}) : 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
|
|
318
319
|
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
319
320
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
320
321
|
}
|
|
322
|
+
// Actual block building
|
|
321
323
|
this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
|
|
322
|
-
this.metrics.incOpenSlot(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) {
|
|
323
367
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
324
|
-
proposer
|
|
325
|
-
coinbase,
|
|
368
|
+
proposer,
|
|
326
369
|
publisher: publisher.getSenderAddress(),
|
|
327
|
-
feeRecipient,
|
|
328
370
|
globalVariables: newGlobalVariables.toInspect(),
|
|
329
371
|
chainTipArchive,
|
|
330
372
|
blockNumber: newBlockNumber,
|
|
331
373
|
slot
|
|
332
374
|
});
|
|
333
|
-
// If I created a "partial" header here that should make our job much easier.
|
|
334
375
|
const proposalHeader = CheckpointHeader.from({
|
|
335
376
|
...newGlobalVariables,
|
|
336
377
|
timestamp: newGlobalVariables.timestamp,
|
|
337
378
|
lastArchiveRoot: chainTipArchive,
|
|
379
|
+
blockHeadersHash: Fr.ZERO,
|
|
338
380
|
contentCommitment: ContentCommitment.empty(),
|
|
339
381
|
totalManaUsed: Fr.ZERO
|
|
340
382
|
});
|
|
@@ -345,7 +387,7 @@ export { SequencerState };
|
|
|
345
387
|
// and also we may need to fetch more if we don't have enough valid txs.
|
|
346
388
|
const pendingTxs = this.p2pClient.iteratePendingTxs();
|
|
347
389
|
try {
|
|
348
|
-
block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables,
|
|
390
|
+
block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
|
|
349
391
|
} catch (err) {
|
|
350
392
|
this.emit('block-build-failed', {
|
|
351
393
|
reason: err.message
|
|
@@ -358,6 +400,7 @@ export { SequencerState };
|
|
|
358
400
|
slot
|
|
359
401
|
});
|
|
360
402
|
}
|
|
403
|
+
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
361
404
|
}
|
|
362
405
|
} else {
|
|
363
406
|
this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
|
|
@@ -369,28 +412,13 @@ export { SequencerState };
|
|
|
369
412
|
minTxs: this.minTxsPerBlock,
|
|
370
413
|
availableTxs: pendingTxCount
|
|
371
414
|
});
|
|
415
|
+
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
372
416
|
}
|
|
373
|
-
|
|
374
|
-
enqueueGovernanceSignalPromise,
|
|
375
|
-
enqueueSlashingActionsPromise
|
|
376
|
-
]);
|
|
377
|
-
const l1Response = await publisher.sendRequests();
|
|
378
|
-
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
379
|
-
if (proposedBlock) {
|
|
380
|
-
this.lastBlockPublished = block;
|
|
381
|
-
this.emit('block-published', {
|
|
382
|
-
blockNumber: newBlockNumber,
|
|
383
|
-
slot: Number(slot)
|
|
384
|
-
});
|
|
385
|
-
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
386
|
-
} else if (block) {
|
|
387
|
-
this.emit('block-publish-failed', l1Response ?? {});
|
|
388
|
-
}
|
|
389
|
-
this.setState(SequencerState.IDLE, undefined);
|
|
417
|
+
return block;
|
|
390
418
|
}
|
|
391
|
-
async
|
|
419
|
+
async safeWork() {
|
|
392
420
|
try {
|
|
393
|
-
await this.
|
|
421
|
+
await this.work();
|
|
394
422
|
} catch (err) {
|
|
395
423
|
if (err instanceof SequencerTooSlowError) {
|
|
396
424
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
@@ -459,7 +487,7 @@ export { SequencerState };
|
|
|
459
487
|
maxTransactions: this.maxTxsPerBlock,
|
|
460
488
|
maxBlockSize: this.maxBlockSizeInBytes,
|
|
461
489
|
maxBlockGas: this.maxBlockGas,
|
|
462
|
-
maxBlobFields:
|
|
490
|
+
maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
|
|
463
491
|
deadline
|
|
464
492
|
};
|
|
465
493
|
}
|
|
@@ -476,12 +504,13 @@ export { SequencerState };
|
|
|
476
504
|
*/ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock, publisher) {
|
|
477
505
|
await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
478
506
|
const blockNumber = newGlobalVariables.blockNumber;
|
|
479
|
-
const
|
|
480
|
-
const
|
|
507
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
|
|
508
|
+
const slot = proposalHeader.slotNumber;
|
|
509
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
481
510
|
const workTimer = new Timer();
|
|
482
511
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
483
512
|
try {
|
|
484
|
-
const blockBuilderOptions = this.getBlockBuilderOptions(
|
|
513
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(slot);
|
|
485
514
|
const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
|
|
486
515
|
const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
|
|
487
516
|
const blockBuildDuration = workTimer.ms();
|
|
@@ -514,16 +543,22 @@ export { SequencerState };
|
|
|
514
543
|
txHashes,
|
|
515
544
|
...blockStats
|
|
516
545
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
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}`, {
|
|
521
555
|
blockHash,
|
|
522
|
-
blockNumber
|
|
556
|
+
blockNumber,
|
|
557
|
+
slot
|
|
523
558
|
});
|
|
524
559
|
}
|
|
525
|
-
|
|
526
|
-
const attestationsAndSignersSignature = this.validatorClient ? await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress())
|
|
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());
|
|
527
562
|
await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
|
|
528
563
|
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
529
564
|
return block;
|
|
@@ -533,38 +568,36 @@ export { SequencerState };
|
|
|
533
568
|
}
|
|
534
569
|
}
|
|
535
570
|
async collectAttestations(block, txs, proposerAddress) {
|
|
536
|
-
const { committee } = await this.epochCache.getCommittee(block.
|
|
571
|
+
const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
|
|
537
572
|
// We checked above that the committee is defined, so this should never happen.
|
|
538
573
|
if (!committee) {
|
|
539
574
|
throw new Error('No committee when collecting attestations');
|
|
540
575
|
}
|
|
541
576
|
if (committee.length === 0) {
|
|
542
577
|
this.log.verbose(`Attesting committee is empty`);
|
|
543
|
-
return
|
|
578
|
+
return CommitteeAttestationsAndSigners.empty();
|
|
544
579
|
} else {
|
|
545
580
|
this.log.debug(`Attesting committee length is ${committee.length}`);
|
|
546
581
|
}
|
|
547
582
|
if (!this.validatorClient) {
|
|
548
|
-
|
|
549
|
-
this.log.error(msg);
|
|
550
|
-
throw new Error(msg);
|
|
583
|
+
throw new Error('Missing validator client: Cannot collect attestations');
|
|
551
584
|
}
|
|
552
585
|
const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
|
|
553
|
-
const slotNumber = block.header.globalVariables.slotNumber
|
|
586
|
+
const slotNumber = block.header.globalVariables.slotNumber;
|
|
554
587
|
this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
|
|
555
588
|
this.log.debug('Creating block proposal for validators');
|
|
556
589
|
const blockProposalOptions = {
|
|
557
590
|
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
558
591
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
559
592
|
};
|
|
560
|
-
const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), block.archive.root,
|
|
593
|
+
const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), block.archive.root, txs, proposerAddress, blockProposalOptions);
|
|
561
594
|
if (!proposal) {
|
|
562
595
|
throw new Error(`Failed to create block proposal`);
|
|
563
596
|
}
|
|
564
597
|
if (this.config.skipCollectingAttestations) {
|
|
565
598
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
566
599
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
567
|
-
return orderAttestations(attestations ?? [], committee);
|
|
600
|
+
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
568
601
|
}
|
|
569
602
|
this.log.debug('Broadcasting block proposal to validators');
|
|
570
603
|
await this.validatorClient.broadcastBlockProposal(proposal);
|
|
@@ -578,13 +611,11 @@ export { SequencerState };
|
|
|
578
611
|
collectedAttestationsCount = attestations.length;
|
|
579
612
|
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
580
613
|
const sorted = orderAttestations(attestations, committee);
|
|
581
|
-
if
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
this.log.warn(`Injecting fake attestation in block ${block.number}`);
|
|
585
|
-
unfreeze(nonEmpty[randomIndex]).signature = Signature.random();
|
|
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);
|
|
586
617
|
}
|
|
587
|
-
return sorted;
|
|
618
|
+
return new CommitteeAttestationsAndSigners(sorted);
|
|
588
619
|
} catch (err) {
|
|
589
620
|
if (err && err instanceof AttestationTimeoutError) {
|
|
590
621
|
collectedAttestationsCount = err.collectedCount;
|
|
@@ -594,14 +625,51 @@ export { SequencerState };
|
|
|
594
625
|
this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
|
|
595
626
|
}
|
|
596
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
|
+
}
|
|
597
665
|
/**
|
|
598
666
|
* Publishes the L2Block to the rollup contract.
|
|
599
667
|
* @param block - The L2Block to be published.
|
|
600
668
|
*/ async enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher) {
|
|
601
669
|
// Publishes new block to the network and awaits the tx to be mined
|
|
602
|
-
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber
|
|
670
|
+
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
|
|
603
671
|
// Time out tx at the end of the slot
|
|
604
|
-
const slot = block.header.globalVariables.slotNumber
|
|
672
|
+
const slot = block.header.globalVariables.slotNumber;
|
|
605
673
|
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
606
674
|
const enqueued = await publisher.enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, {
|
|
607
675
|
txTimeoutAt,
|
|
@@ -614,8 +682,20 @@ export { SequencerState };
|
|
|
614
682
|
/**
|
|
615
683
|
* Returns whether all dependencies have caught up.
|
|
616
684
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
617
|
-
|
|
618
|
-
|
|
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
|
+
}
|
|
619
699
|
const syncedBlocks = await Promise.all([
|
|
620
700
|
this.worldState.status().then(({ syncSummary })=>({
|
|
621
701
|
number: syncSummary.latestBlockNumber,
|
|
@@ -624,54 +704,205 @@ export { SequencerState };
|
|
|
624
704
|
this.l2BlockSource.getL2Tips().then((t)=>t.latest),
|
|
625
705
|
this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
|
|
626
706
|
this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
|
|
627
|
-
this.l2BlockSource.getL1Timestamp(),
|
|
628
707
|
this.l2BlockSource.getPendingChainValidationStatus()
|
|
629
708
|
]);
|
|
630
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource,
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
const result = l2BlockSource.
|
|
634
|
-
const logData = {
|
|
635
|
-
worldState,
|
|
636
|
-
l2BlockSource,
|
|
637
|
-
p2p,
|
|
638
|
-
l1ToL2MessageSource
|
|
639
|
-
};
|
|
640
|
-
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;
|
|
641
713
|
if (!result) {
|
|
714
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
715
|
+
worldState,
|
|
716
|
+
l2BlockSource,
|
|
717
|
+
p2p,
|
|
718
|
+
l1ToL2MessageSource
|
|
719
|
+
});
|
|
642
720
|
return undefined;
|
|
643
721
|
}
|
|
722
|
+
// Special case for genesis state
|
|
644
723
|
const blockNumber = worldState.number;
|
|
645
|
-
if (blockNumber
|
|
646
|
-
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
647
|
-
if (!block) {
|
|
648
|
-
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
649
|
-
this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
|
|
650
|
-
return undefined;
|
|
651
|
-
}
|
|
652
|
-
return {
|
|
653
|
-
block,
|
|
654
|
-
blockNumber: block.number,
|
|
655
|
-
archive: block.archive.root,
|
|
656
|
-
l1Timestamp,
|
|
657
|
-
pendingChainValidationStatus
|
|
658
|
-
};
|
|
659
|
-
} else {
|
|
724
|
+
if (blockNumber < INITIAL_L2_BLOCK_NUM) {
|
|
660
725
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
661
726
|
return {
|
|
662
|
-
blockNumber: INITIAL_L2_BLOCK_NUM - 1,
|
|
727
|
+
blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1),
|
|
663
728
|
archive,
|
|
664
729
|
l1Timestamp,
|
|
665
730
|
pendingChainValidationStatus
|
|
666
731
|
};
|
|
667
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();
|
|
668
899
|
}
|
|
669
900
|
/**
|
|
670
901
|
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|
|
671
902
|
* has been there without being invalidated and whether the sequencer is in the committee or not. We always
|
|
672
903
|
* have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
|
|
673
904
|
* and if they fail, any sequencer will try as well.
|
|
674
|
-
*/ async considerInvalidatingBlock(syncedTo, currentSlot
|
|
905
|
+
*/ async considerInvalidatingBlock(syncedTo, currentSlot) {
|
|
675
906
|
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
676
907
|
if (pendingChainValidationStatus.valid) {
|
|
677
908
|
return;
|
|
@@ -679,6 +910,7 @@ export { SequencerState };
|
|
|
679
910
|
const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
|
|
680
911
|
const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
|
|
681
912
|
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
|
|
913
|
+
const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
682
914
|
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
|
|
683
915
|
const logData = {
|
|
684
916
|
invalidL1Timestamp: invalidBlockTimestamp,
|
|
@@ -696,6 +928,21 @@ export { SequencerState };
|
|
|
696
928
|
this.log.debug(`Not invalidating pending chain`, logData);
|
|
697
929
|
return;
|
|
698
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);
|
|
699
946
|
const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
700
947
|
if (!invalidateBlock) {
|
|
701
948
|
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
@@ -703,7 +950,12 @@ export { SequencerState };
|
|
|
703
950
|
}
|
|
704
951
|
this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
|
|
705
952
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
706
|
-
|
|
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
|
+
}
|
|
707
959
|
}
|
|
708
960
|
getSlotStartBuildTimestamp(slotNumber) {
|
|
709
961
|
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
|
@@ -724,7 +976,7 @@ export { SequencerState };
|
|
|
724
976
|
}
|
|
725
977
|
_ts_decorate([
|
|
726
978
|
trackSpan('Sequencer.work')
|
|
727
|
-
], Sequencer.prototype, "
|
|
979
|
+
], Sequencer.prototype, "safeWork", null);
|
|
728
980
|
_ts_decorate([
|
|
729
981
|
trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
|
|
730
982
|
[Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber
|