@aztec/sequencer-client 0.0.0-test.1 → 0.0.1-commit.5476d83
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 +26 -26
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +66 -51
- package/dest/config.d.ts +7 -15
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +59 -54
- package/dest/global_variable_builder/global_builder.d.ts +12 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +43 -35
- package/dest/global_variable_builder/index.d.ts +1 -1
- package/dest/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/publisher/config.d.ts +9 -9
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +24 -17
- package/dest/publisher/index.d.ts +3 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +3 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +43 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -0
- package/dest/publisher/sequencer-publisher-factory.js +51 -0
- package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +37 -2
- package/dest/publisher/sequencer-publisher.d.ts +112 -73
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +681 -236
- package/dest/sequencer/block_builder.d.ts +27 -0
- package/dest/sequencer/block_builder.d.ts.map +1 -0
- package/dest/sequencer/block_builder.js +134 -0
- package/dest/sequencer/config.d.ts +6 -1
- 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 +2 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +1 -1
- package/dest/sequencer/metrics.d.ts +28 -12
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +122 -50
- package/dest/sequencer/sequencer.d.ts +122 -91
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +723 -370
- package/dest/sequencer/timetable.d.ts +33 -21
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +57 -30
- package/dest/sequencer/utils.d.ts +12 -36
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +9 -47
- package/dest/test/index.d.ts +8 -1
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +0 -4
- package/dest/tx_validator/nullifier_cache.d.ts +1 -3
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +10 -11
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +27 -24
- package/package.json +45 -45
- package/src/client/sequencer-client.ts +95 -85
- package/src/config.ts +67 -61
- package/src/global_variable_builder/global_builder.ts +52 -27
- package/src/index.ts +6 -2
- package/src/publisher/config.ts +34 -24
- package/src/publisher/index.ts +4 -0
- package/src/publisher/sequencer-publisher-factory.ts +91 -0
- package/src/publisher/sequencer-publisher-metrics.ts +24 -2
- package/src/publisher/sequencer-publisher.ts +817 -268
- package/src/sequencer/block_builder.ts +222 -0
- package/src/sequencer/config.ts +7 -0
- package/src/sequencer/errors.ts +21 -0
- package/src/sequencer/index.ts +1 -1
- package/src/sequencer/metrics.ts +156 -53
- package/src/sequencer/sequencer.ts +918 -423
- package/src/sequencer/timetable.ts +98 -33
- package/src/sequencer/utils.ts +17 -58
- package/src/test/index.ts +11 -4
- package/src/tx_validator/tx_validator_factory.ts +44 -32
- package/dest/sequencer/allowed.d.ts +0 -3
- package/dest/sequencer/allowed.d.ts.map +0 -1
- package/dest/sequencer/allowed.js +0 -27
- package/dest/slasher/factory.d.ts +0 -7
- package/dest/slasher/factory.d.ts.map +0 -1
- package/dest/slasher/factory.js +0 -8
- package/dest/slasher/index.d.ts +0 -3
- package/dest/slasher/index.d.ts.map +0 -1
- package/dest/slasher/index.js +0 -2
- package/dest/slasher/slasher_client.d.ts +0 -75
- package/dest/slasher/slasher_client.d.ts.map +0 -1
- package/dest/slasher/slasher_client.js +0 -132
- package/dest/tx_validator/archive_cache.d.ts +0 -14
- package/dest/tx_validator/archive_cache.d.ts.map +0 -1
- package/dest/tx_validator/archive_cache.js +0 -22
- package/dest/tx_validator/gas_validator.d.ts +0 -14
- package/dest/tx_validator/gas_validator.d.ts.map +0 -1
- package/dest/tx_validator/gas_validator.js +0 -78
- package/dest/tx_validator/phases_validator.d.ts +0 -12
- package/dest/tx_validator/phases_validator.d.ts.map +0 -1
- package/dest/tx_validator/phases_validator.js +0 -80
- package/dest/tx_validator/test_utils.d.ts +0 -23
- package/dest/tx_validator/test_utils.d.ts.map +0 -1
- package/dest/tx_validator/test_utils.js +0 -26
- package/src/sequencer/allowed.ts +0 -36
- package/src/slasher/factory.ts +0 -15
- package/src/slasher/index.ts +0 -2
- package/src/slasher/slasher_client.ts +0 -193
- package/src/tx_validator/archive_cache.ts +0 -28
- package/src/tx_validator/gas_validator.ts +0 -101
- package/src/tx_validator/phases_validator.ts +0 -98
- package/src/tx_validator/test_utils.ts +0 -48
|
@@ -4,26 +4,33 @@ 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 { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
8
|
-
import {
|
|
7
|
+
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
8
|
+
import { FormattedViemError, NoCommitteeError } from '@aztec/ethereum';
|
|
9
|
+
import { omit, pick } from '@aztec/foundation/collection';
|
|
10
|
+
import { randomInt } from '@aztec/foundation/crypto';
|
|
9
11
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
12
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
10
13
|
import { Fr } from '@aztec/foundation/fields';
|
|
11
14
|
import { createLogger } from '@aztec/foundation/log';
|
|
12
15
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
13
|
-
import { Timer
|
|
14
|
-
import {
|
|
16
|
+
import { Timer } from '@aztec/foundation/timer';
|
|
17
|
+
import { unfreeze } from '@aztec/foundation/types';
|
|
18
|
+
import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
|
|
19
|
+
import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
15
20
|
import { Gas } from '@aztec/stdlib/gas';
|
|
16
21
|
import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
|
|
22
|
+
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
23
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
17
24
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
25
|
+
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
26
|
+
import { ContentCommitment } from '@aztec/stdlib/tx';
|
|
27
|
+
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
20
28
|
import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import { getDefaultAllowedSetupFunctions } from './allowed.js';
|
|
29
|
+
import EventEmitter from 'node:events';
|
|
30
|
+
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
24
31
|
import { SequencerMetrics } from './metrics.js';
|
|
25
|
-
import { SequencerTimetable
|
|
26
|
-
import { SequencerState
|
|
32
|
+
import { SequencerTimetable } from './timetable.js';
|
|
33
|
+
import { SequencerState } from './utils.js';
|
|
27
34
|
export { SequencerState };
|
|
28
35
|
/**
|
|
29
36
|
* Sequencer client
|
|
@@ -33,80 +40,68 @@ export { SequencerState };
|
|
|
33
40
|
* - Adds proof requests to the request pool (not for this milestone).
|
|
34
41
|
* - Receives results to those proofs from the network (repeats as necessary) (not for this milestone).
|
|
35
42
|
* - Publishes L1 tx(s) to the rollup contract via RollupPublisher.
|
|
36
|
-
*/ export class Sequencer {
|
|
37
|
-
|
|
43
|
+
*/ export class Sequencer extends EventEmitter {
|
|
44
|
+
publisherFactory;
|
|
38
45
|
validatorClient;
|
|
39
46
|
globalsBuilder;
|
|
40
47
|
p2pClient;
|
|
41
48
|
worldState;
|
|
42
49
|
slasherClient;
|
|
43
|
-
blockBuilderFactory;
|
|
44
50
|
l2BlockSource;
|
|
45
51
|
l1ToL2MessageSource;
|
|
46
|
-
|
|
47
|
-
contractDataSource;
|
|
52
|
+
blockBuilder;
|
|
48
53
|
l1Constants;
|
|
49
54
|
dateProvider;
|
|
55
|
+
epochCache;
|
|
56
|
+
rollupContract;
|
|
50
57
|
config;
|
|
58
|
+
telemetry;
|
|
51
59
|
log;
|
|
52
60
|
runningPromise;
|
|
53
61
|
pollingIntervalMs;
|
|
54
62
|
maxTxsPerBlock;
|
|
55
63
|
minTxsPerBlock;
|
|
56
64
|
maxL1TxInclusionTimeIntoSlot;
|
|
57
|
-
// TODO: zero values should not be allowed for the following 2 values in PROD
|
|
58
|
-
_coinbase;
|
|
59
|
-
_feeRecipient;
|
|
60
65
|
state;
|
|
61
|
-
allowedInSetup;
|
|
62
66
|
maxBlockSizeInBytes;
|
|
63
67
|
maxBlockGas;
|
|
64
68
|
metrics;
|
|
65
|
-
|
|
69
|
+
lastBlockPublished;
|
|
70
|
+
governanceProposerPayload;
|
|
71
|
+
/** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
|
|
72
|
+
/** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */ lastSlotForValidationBlock;
|
|
66
73
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
67
74
|
enforceTimeTable;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.
|
|
81
|
-
|
|
82
|
-
this.config
|
|
83
|
-
this.log = log;
|
|
84
|
-
this.pollingIntervalMs = 1000;
|
|
85
|
-
this.maxTxsPerBlock = 32;
|
|
86
|
-
this.minTxsPerBlock = 1;
|
|
87
|
-
this.maxL1TxInclusionTimeIntoSlot = 0;
|
|
88
|
-
this._coinbase = EthAddress.ZERO;
|
|
89
|
-
this._feeRecipient = AztecAddress.ZERO;
|
|
90
|
-
this.state = SequencerState.STOPPED;
|
|
91
|
-
this.allowedInSetup = [];
|
|
92
|
-
this.maxBlockSizeInBytes = 1024 * 1024;
|
|
93
|
-
this.maxBlockGas = new Gas(100e9, 100e9);
|
|
94
|
-
this.isFlushing = false;
|
|
95
|
-
this.enforceTimeTable = false;
|
|
96
|
-
this.metrics = new SequencerMetrics(telemetry, ()=>this.state, 'Sequencer');
|
|
97
|
-
// Register the block builder with the validator client for re-execution
|
|
98
|
-
this.validatorClient?.registerBlockBuilder(this.buildBlock.bind(this));
|
|
99
|
-
// Register the slasher on the publisher to fetch slashing payloads
|
|
100
|
-
this.publisher.registerSlashPayloadGetter(this.slasherClient.getSlashPayload.bind(this.slasherClient));
|
|
75
|
+
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
76
|
+
// But we have a number of tests that abuse/rely on this class having a permanent publisher.
|
|
77
|
+
// As long as those tests only configure a single publisher they will continue to work.
|
|
78
|
+
// This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
|
|
79
|
+
// for the block proposer.
|
|
80
|
+
publisher;
|
|
81
|
+
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
82
|
+
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;
|
|
83
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
84
|
+
if (this.config.fishermanMode) {
|
|
85
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
86
|
+
}
|
|
87
|
+
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
88
|
+
// Initialize config
|
|
89
|
+
this.updateConfig(this.config);
|
|
101
90
|
}
|
|
102
91
|
get tracer() {
|
|
103
92
|
return this.metrics.tracer;
|
|
104
93
|
}
|
|
94
|
+
getValidatorAddresses() {
|
|
95
|
+
return this.validatorClient?.getValidatorAddresses();
|
|
96
|
+
}
|
|
97
|
+
getConfig() {
|
|
98
|
+
return this.config;
|
|
99
|
+
}
|
|
105
100
|
/**
|
|
106
|
-
* Updates sequencer config.
|
|
101
|
+
* Updates sequencer config by the defined values in the config on input.
|
|
107
102
|
* @param config - New parameters.
|
|
108
|
-
*/
|
|
109
|
-
this.log.info(`Sequencer config set`, omit(pickFromSchema(config, SequencerConfigSchema), '
|
|
103
|
+
*/ updateConfig(config) {
|
|
104
|
+
this.log.info(`Sequencer config set`, omit(pickFromSchema(config, SequencerConfigSchema), 'txPublicSetupAllowList'));
|
|
110
105
|
if (config.transactionPollingIntervalMS !== undefined) {
|
|
111
106
|
this.pollingIntervalMs = config.transactionPollingIntervalMS;
|
|
112
107
|
}
|
|
@@ -122,22 +117,11 @@ export { SequencerState };
|
|
|
122
117
|
if (config.maxL2BlockGas !== undefined) {
|
|
123
118
|
this.maxBlockGas = new Gas(this.maxBlockGas.daGas, config.maxL2BlockGas);
|
|
124
119
|
}
|
|
125
|
-
if (config.coinbase) {
|
|
126
|
-
this._coinbase = config.coinbase;
|
|
127
|
-
}
|
|
128
|
-
if (config.feeRecipient) {
|
|
129
|
-
this._feeRecipient = config.feeRecipient;
|
|
130
|
-
}
|
|
131
|
-
if (config.allowedInSetup) {
|
|
132
|
-
this.allowedInSetup = config.allowedInSetup;
|
|
133
|
-
} else {
|
|
134
|
-
this.allowedInSetup = await getDefaultAllowedSetupFunctions();
|
|
135
|
-
}
|
|
136
120
|
if (config.maxBlockSizeInBytes !== undefined) {
|
|
137
121
|
this.maxBlockSizeInBytes = config.maxBlockSizeInBytes;
|
|
138
122
|
}
|
|
139
123
|
if (config.governanceProposerPayload) {
|
|
140
|
-
this.
|
|
124
|
+
this.governanceProposerPayload = config.governanceProposerPayload;
|
|
141
125
|
}
|
|
142
126
|
if (config.maxL1TxInclusionTimeIntoSlot !== undefined) {
|
|
143
127
|
this.maxL1TxInclusionTimeIntoSlot = config.maxL1TxInclusionTimeIntoSlot;
|
|
@@ -147,43 +131,46 @@ export { SequencerState };
|
|
|
147
131
|
}
|
|
148
132
|
this.setTimeTable();
|
|
149
133
|
// TODO: Just read everything from the config object as needed instead of copying everything into local vars.
|
|
150
|
-
this.config
|
|
134
|
+
// Update all values on this.config that are populated in the config object.
|
|
135
|
+
Object.assign(this.config, config);
|
|
151
136
|
}
|
|
152
137
|
setTimeTable() {
|
|
153
|
-
this.timetable = new SequencerTimetable(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
138
|
+
this.timetable = new SequencerTimetable({
|
|
139
|
+
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
140
|
+
aztecSlotDuration: this.aztecSlotDuration,
|
|
141
|
+
maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
|
|
142
|
+
attestationPropagationTime: this.config.attestationPropagationTime,
|
|
143
|
+
enforce: this.enforceTimeTable
|
|
144
|
+
}, this.metrics, this.log);
|
|
145
|
+
}
|
|
146
|
+
async init() {
|
|
147
|
+
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
157
148
|
}
|
|
158
149
|
/**
|
|
159
150
|
* Starts the sequencer and moves to IDLE state.
|
|
160
|
-
*/
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
|
|
151
|
+
*/ start() {
|
|
152
|
+
this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
|
|
153
|
+
this.setState(SequencerState.IDLE, undefined, {
|
|
154
|
+
force: true
|
|
155
|
+
});
|
|
164
156
|
this.runningPromise.start();
|
|
165
|
-
this.log.info(
|
|
157
|
+
this.log.info('Started sequencer');
|
|
166
158
|
}
|
|
167
159
|
/**
|
|
168
160
|
* Stops the sequencer from processing txs and moves to STOPPED state.
|
|
169
161
|
*/ async stop() {
|
|
170
|
-
this.log.
|
|
171
|
-
|
|
162
|
+
this.log.info(`Stopping sequencer`);
|
|
163
|
+
this.setState(SequencerState.STOPPING, undefined, {
|
|
164
|
+
force: true
|
|
165
|
+
});
|
|
166
|
+
this.publisher?.interrupt();
|
|
172
167
|
await this.runningPromise?.stop();
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
this.setState(SequencerState.STOPPED, undefined, {
|
|
169
|
+
force: true
|
|
170
|
+
});
|
|
176
171
|
this.log.info('Stopped sequencer');
|
|
177
172
|
}
|
|
178
173
|
/**
|
|
179
|
-
* Starts a previously stopped sequencer.
|
|
180
|
-
*/ restart() {
|
|
181
|
-
this.log.info('Restarting sequencer');
|
|
182
|
-
this.publisher.restart();
|
|
183
|
-
this.runningPromise.start();
|
|
184
|
-
this.setState(SequencerState.IDLE, 0n, true);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
174
|
* Returns the current state of the sequencer.
|
|
188
175
|
* @returns An object with a state entry with one of SequencerState.
|
|
189
176
|
*/ status() {
|
|
@@ -191,9 +178,6 @@ export { SequencerState };
|
|
|
191
178
|
state: this.state
|
|
192
179
|
};
|
|
193
180
|
}
|
|
194
|
-
/** Forces the sequencer to bypass all time and tx count checks for the next block and build anyway. */ flush() {
|
|
195
|
-
this.isFlushing = true;
|
|
196
|
-
}
|
|
197
181
|
/**
|
|
198
182
|
* @notice Performs most of the sequencer duties:
|
|
199
183
|
* - Checks if we are up to date
|
|
@@ -201,231 +185,306 @@ export { SequencerState };
|
|
|
201
185
|
* - Collect attestations for the block
|
|
202
186
|
* - Submit block
|
|
203
187
|
* - If our block for some reason is not included, revert the state
|
|
204
|
-
*/ async
|
|
205
|
-
this.setState(SequencerState.SYNCHRONIZING,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
188
|
+
*/ async work() {
|
|
189
|
+
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
190
|
+
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
191
|
+
// Check we have not already published a block for this slot (cheapest check)
|
|
192
|
+
if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
|
|
193
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`);
|
|
210
194
|
return;
|
|
211
195
|
}
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!
|
|
218
|
-
this.
|
|
196
|
+
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
197
|
+
const syncedTo = await this.checkSync({
|
|
198
|
+
ts,
|
|
199
|
+
slot
|
|
200
|
+
});
|
|
201
|
+
if (!syncedTo) {
|
|
202
|
+
await this.tryVoteWhenSyncFails({
|
|
203
|
+
slot,
|
|
204
|
+
ts
|
|
205
|
+
});
|
|
219
206
|
return;
|
|
220
207
|
}
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
208
|
+
const chainTipArchive = syncedTo.archive;
|
|
209
|
+
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
210
|
+
const syncLogData = {
|
|
211
|
+
now,
|
|
212
|
+
syncedToL1Ts: syncedTo.l1Timestamp,
|
|
213
|
+
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
214
|
+
nextL2Slot: slot,
|
|
215
|
+
nextL2SlotTs: ts,
|
|
216
|
+
l1SlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
217
|
+
newBlockNumber,
|
|
218
|
+
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
|
|
219
|
+
};
|
|
220
|
+
// Check that we are a proposer for the next slot
|
|
221
|
+
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
222
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
223
|
+
// If we are not a proposer check if we should invalidate a invalid block, and bail
|
|
224
|
+
if (!canPropose) {
|
|
225
|
+
await this.considerInvalidatingBlock(syncedTo, slot);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
|
|
229
|
+
if (this.config.fishermanMode) {
|
|
230
|
+
if (this.lastSlotForValidationBlock === slot) {
|
|
231
|
+
this.log.trace(`Already validated block building for slot ${slot} (skipping)`, {
|
|
232
|
+
slot
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.log.debug(`Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`, {
|
|
237
|
+
slot,
|
|
238
|
+
proposer: proposer?.toString()
|
|
239
|
+
});
|
|
240
|
+
// Mark this slot as being validated
|
|
241
|
+
this.lastSlotForValidationBlock = slot;
|
|
242
|
+
}
|
|
243
|
+
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
244
|
+
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
245
|
+
this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
246
|
+
...syncLogData,
|
|
247
|
+
block: syncedTo.block.header.toInspect()
|
|
248
|
+
});
|
|
249
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// We now need to get ourselves a publisher.
|
|
253
|
+
// The returned attestor will be the one we provided if we provided one.
|
|
254
|
+
// Otherwise it will be a valid attestor for the returned publisher.
|
|
255
|
+
// In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
|
|
256
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(this.config.fishermanMode ? undefined : proposer);
|
|
257
|
+
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
258
|
+
this.publisher = publisher;
|
|
259
|
+
// In fisherman mode, set the actual proposer's address for simulations
|
|
260
|
+
if (this.config.fishermanMode) {
|
|
261
|
+
if (proposer) {
|
|
262
|
+
publisher.setProposerAddressForSimulation(proposer);
|
|
263
|
+
this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Get proposer credentials
|
|
267
|
+
const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
|
|
268
|
+
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
|
|
269
|
+
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
270
|
+
const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
|
|
271
|
+
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
272
|
+
// if all the previous checks are good, but we do it just in case.
|
|
273
|
+
const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposer ?? EthAddress.ZERO, invalidateBlock);
|
|
274
|
+
if (canProposeCheck === undefined) {
|
|
275
|
+
this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
|
|
276
|
+
this.emit('proposer-rollup-check-failed', {
|
|
277
|
+
reason: 'Rollup contract check failed'
|
|
278
|
+
});
|
|
279
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
280
|
+
return;
|
|
281
|
+
} else if (canProposeCheck.slot !== slot) {
|
|
282
|
+
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}.`, {
|
|
283
|
+
...syncLogData,
|
|
284
|
+
rollup: canProposeCheck,
|
|
285
|
+
newBlockNumber,
|
|
286
|
+
expectedSlot: slot
|
|
287
|
+
});
|
|
288
|
+
this.emit('proposer-rollup-check-failed', {
|
|
289
|
+
reason: 'Slot mismatch'
|
|
290
|
+
});
|
|
291
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
292
|
+
return;
|
|
293
|
+
} else if (canProposeCheck.checkpointNumber !== BigInt(newBlockNumber)) {
|
|
294
|
+
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}.`, {
|
|
295
|
+
...syncLogData,
|
|
296
|
+
rollup: canProposeCheck,
|
|
297
|
+
newBlockNumber,
|
|
298
|
+
expectedSlot: slot
|
|
299
|
+
});
|
|
300
|
+
this.emit('proposer-rollup-check-failed', {
|
|
301
|
+
reason: 'Block mismatch'
|
|
302
|
+
});
|
|
303
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
|
|
307
|
+
...syncLogData
|
|
308
|
+
});
|
|
309
|
+
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
|
|
310
|
+
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
311
|
+
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
312
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
|
|
313
|
+
// Enqueues block invalidation
|
|
314
|
+
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
315
|
+
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
316
|
+
}
|
|
317
|
+
// Actual block building
|
|
225
318
|
this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
|
|
319
|
+
this.metrics.incOpenSlot(slot, proposer?.toString() ?? 'unknown');
|
|
320
|
+
const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
|
|
321
|
+
// Wait until the voting promises have resolved, so all requests are enqueued
|
|
322
|
+
await Promise.all(votesPromises);
|
|
323
|
+
// In fisherman mode, we don't publish to L1
|
|
324
|
+
if (this.config.fishermanMode) {
|
|
325
|
+
// Clear pending requests
|
|
326
|
+
publisher.clearPendingRequests();
|
|
327
|
+
if (block) {
|
|
328
|
+
this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
|
|
329
|
+
blockNumber: newBlockNumber,
|
|
330
|
+
slot: Number(slot),
|
|
331
|
+
archive: block.archive.toString(),
|
|
332
|
+
txCount: block.body.txEffects.length
|
|
333
|
+
});
|
|
334
|
+
this.lastBlockPublished = block;
|
|
335
|
+
this.metrics.recordBlockProposalSuccess();
|
|
336
|
+
} else {
|
|
337
|
+
// Block building failed in fisherman mode
|
|
338
|
+
this.log.warn(`Validation block building FAILED for slot ${slot}`, {
|
|
339
|
+
blockNumber: newBlockNumber,
|
|
340
|
+
slot: Number(slot)
|
|
341
|
+
});
|
|
342
|
+
this.metrics.recordBlockProposalFailed('block_build_failed');
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Normal mode: send the tx to L1
|
|
346
|
+
const l1Response = await publisher.sendRequests();
|
|
347
|
+
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
348
|
+
if (proposedBlock) {
|
|
349
|
+
this.lastBlockPublished = block;
|
|
350
|
+
this.emit('block-published', {
|
|
351
|
+
blockNumber: newBlockNumber,
|
|
352
|
+
slot: Number(slot)
|
|
353
|
+
});
|
|
354
|
+
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
355
|
+
} else if (block) {
|
|
356
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
360
|
+
}
|
|
361
|
+
/** Tries building a block proposal, and if successful, enqueues it for publishing. */ async tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock) {
|
|
226
362
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
363
|
+
proposer,
|
|
364
|
+
publisher: publisher.getSenderAddress(),
|
|
365
|
+
globalVariables: newGlobalVariables.toInspect(),
|
|
227
366
|
chainTipArchive,
|
|
228
367
|
blockNumber: newBlockNumber,
|
|
229
368
|
slot
|
|
230
369
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
370
|
+
const proposalHeader = CheckpointHeader.from({
|
|
371
|
+
...newGlobalVariables,
|
|
372
|
+
timestamp: newGlobalVariables.timestamp,
|
|
373
|
+
lastArchiveRoot: chainTipArchive,
|
|
374
|
+
blockHeadersHash: Fr.ZERO,
|
|
375
|
+
contentCommitment: ContentCommitment.empty(),
|
|
376
|
+
totalManaUsed: Fr.ZERO
|
|
377
|
+
});
|
|
378
|
+
let block;
|
|
234
379
|
const pendingTxCount = await this.p2pClient.getPendingTxCount();
|
|
235
|
-
if (pendingTxCount >= this.minTxsPerBlock
|
|
380
|
+
if (pendingTxCount >= this.minTxsPerBlock) {
|
|
236
381
|
// We don't fetch exactly maxTxsPerBlock txs here because we may not need all of them if we hit a limit before,
|
|
237
382
|
// and also we may need to fetch more if we don't have enough valid txs.
|
|
238
383
|
const pendingTxs = this.p2pClient.iteratePendingTxs();
|
|
239
|
-
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
|
|
384
|
+
try {
|
|
385
|
+
block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
this.emit('block-build-failed', {
|
|
388
|
+
reason: err.message
|
|
243
389
|
});
|
|
244
|
-
|
|
245
|
-
|
|
390
|
+
if (err instanceof FormattedViemError) {
|
|
391
|
+
this.log.verbose(`Unable to build/enqueue block ${err.message}`);
|
|
392
|
+
} else {
|
|
393
|
+
this.log.error(`Error building/enqueuing block`, err, {
|
|
394
|
+
blockNumber: newBlockNumber,
|
|
395
|
+
slot
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
399
|
+
}
|
|
246
400
|
} else {
|
|
247
|
-
this.log.
|
|
248
|
-
|
|
249
|
-
await enqueueGovernanceVotePromise.catch((err)=>{
|
|
250
|
-
this.log.error(`Error enqueuing governance vote`, err, {
|
|
401
|
+
this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
|
|
402
|
+
chainTipArchive,
|
|
251
403
|
blockNumber: newBlockNumber,
|
|
252
404
|
slot
|
|
253
405
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
blockNumber: newBlockNumber,
|
|
258
|
-
slot
|
|
406
|
+
this.emit('tx-count-check-failed', {
|
|
407
|
+
minTxs: this.minTxsPerBlock,
|
|
408
|
+
availableTxs: pendingTxCount
|
|
259
409
|
});
|
|
260
|
-
|
|
261
|
-
await this.publisher.sendRequests();
|
|
262
|
-
if (finishedFlushing) {
|
|
263
|
-
this.isFlushing = false;
|
|
410
|
+
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
264
411
|
}
|
|
265
|
-
|
|
412
|
+
return block;
|
|
266
413
|
}
|
|
267
|
-
async
|
|
414
|
+
async safeWork() {
|
|
268
415
|
try {
|
|
269
|
-
await this.
|
|
416
|
+
await this.work();
|
|
270
417
|
} catch (err) {
|
|
271
418
|
if (err instanceof SequencerTooSlowError) {
|
|
272
|
-
|
|
419
|
+
// Log as warn only if we had to abort halfway through the block proposal
|
|
420
|
+
const logLvl = [
|
|
421
|
+
SequencerState.INITIALIZING_PROPOSAL,
|
|
422
|
+
SequencerState.PROPOSER_CHECK
|
|
423
|
+
].includes(err.proposedState) ? 'debug' : 'warn';
|
|
424
|
+
this.log[logLvl](err.message, {
|
|
425
|
+
now: this.dateProvider.nowInSeconds()
|
|
426
|
+
});
|
|
273
427
|
} else {
|
|
274
428
|
// Re-throw other errors
|
|
275
429
|
throw err;
|
|
276
430
|
}
|
|
277
431
|
} finally{
|
|
278
|
-
this.setState(SequencerState.IDLE,
|
|
432
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
279
433
|
}
|
|
280
434
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
* Checks if we can propose at the next block and returns the slot number if we can.
|
|
286
|
-
* @param tipArchive - The archive of the previous block.
|
|
287
|
-
* @param proposalBlockNumber - The block number of the proposal.
|
|
288
|
-
* @returns The slot number if we can propose at the next block, otherwise undefined.
|
|
289
|
-
*/ async slotForProposal(tipArchive, proposalBlockNumber) {
|
|
290
|
-
const result = await this.publisher.canProposeAtNextEthBlock(tipArchive);
|
|
291
|
-
if (!result) {
|
|
292
|
-
return undefined;
|
|
435
|
+
setState(proposedState, slotNumber, opts = {}) {
|
|
436
|
+
if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
|
|
437
|
+
this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
|
|
438
|
+
throw new SequencerInterruptedError();
|
|
293
439
|
}
|
|
294
|
-
|
|
295
|
-
if (proposalBlockNumber !== blockNumber) {
|
|
296
|
-
const msg = `Sequencer block number mismatch. Expected ${proposalBlockNumber} but got ${blockNumber}.`;
|
|
297
|
-
this.log.warn(msg);
|
|
298
|
-
throw new Error(msg);
|
|
299
|
-
}
|
|
300
|
-
return slot;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
|
|
304
|
-
* @param proposedState - The new state to transition to.
|
|
305
|
-
* @param currentSlotNumber - The current slot number.
|
|
306
|
-
* @param force - Whether to force the transition even if the sequencer is stopped.
|
|
307
|
-
*
|
|
308
|
-
* @dev If the `currentSlotNumber` doesn't matter (e.g. transitioning to IDLE), pass in `0n`;
|
|
309
|
-
* it is only used to check if we have enough time left in the slot to transition to the new state.
|
|
310
|
-
*/ setState(proposedState, currentSlotNumber, force = false) {
|
|
311
|
-
if (this.state === SequencerState.STOPPED && force !== true) {
|
|
440
|
+
if (this.state === SequencerState.STOPPED && !opts.force) {
|
|
312
441
|
this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
|
|
313
442
|
return;
|
|
314
443
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
444
|
+
let secondsIntoSlot = undefined;
|
|
445
|
+
if (slotNumber !== undefined) {
|
|
446
|
+
secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
|
|
447
|
+
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
448
|
+
}
|
|
449
|
+
const boringStates = [
|
|
450
|
+
SequencerState.IDLE,
|
|
451
|
+
SequencerState.SYNCHRONIZING
|
|
452
|
+
];
|
|
453
|
+
const logLevel = boringStates.includes(proposedState) && boringStates.includes(this.state) ? 'trace' : 'debug';
|
|
454
|
+
this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, {
|
|
455
|
+
slotNumber,
|
|
456
|
+
secondsIntoSlot
|
|
457
|
+
});
|
|
458
|
+
this.emit('state-changed', {
|
|
459
|
+
oldState: this.state,
|
|
460
|
+
newState: proposedState,
|
|
461
|
+
secondsIntoSlot,
|
|
462
|
+
slotNumber
|
|
463
|
+
});
|
|
318
464
|
this.state = proposedState;
|
|
319
465
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
* Shared between the sequencer and the validator for re-execution
|
|
324
|
-
*
|
|
325
|
-
* @param pendingTxs - The pending transactions to construct the block from
|
|
326
|
-
* @param newGlobalVariables - The global variables for the new block
|
|
327
|
-
* @param historicalHeader - The historical header of the parent
|
|
328
|
-
* @param opts - Whether to just validate the block as a validator, as opposed to building it as a proposal
|
|
329
|
-
*/ async buildBlock(pendingTxs, newGlobalVariables, opts = {}) {
|
|
330
|
-
const blockNumber = newGlobalVariables.blockNumber.toNumber();
|
|
331
|
-
const slot = newGlobalVariables.slotNumber.toBigInt();
|
|
332
|
-
this.log.debug(`Requesting L1 to L2 messages from contract for block ${blockNumber}`);
|
|
333
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(BigInt(blockNumber));
|
|
334
|
-
const msgCount = l1ToL2Messages.length;
|
|
335
|
-
this.log.verbose(`Building block ${blockNumber} for slot ${slot}`, {
|
|
336
|
-
slot,
|
|
337
|
-
blockNumber,
|
|
338
|
-
msgCount,
|
|
339
|
-
validator: opts.validateOnly
|
|
340
|
-
});
|
|
341
|
-
// Sync to the previous block at least
|
|
342
|
-
await this.worldState.syncImmediate(blockNumber - 1);
|
|
343
|
-
this.log.debug(`Synced to previous block ${blockNumber - 1}`);
|
|
344
|
-
// NB: separating the dbs because both should update the state
|
|
345
|
-
const publicProcessorFork = await this.worldState.fork();
|
|
346
|
-
const orchestratorFork = await this.worldState.fork();
|
|
347
|
-
const previousBlockHeader = (await this.l2BlockSource.getBlock(blockNumber - 1))?.header ?? orchestratorFork.getInitialHeader();
|
|
348
|
-
try {
|
|
349
|
-
const processor = this.publicProcessorFactory.create(publicProcessorFork, newGlobalVariables, true);
|
|
350
|
-
const blockBuildingTimer = new Timer();
|
|
351
|
-
const blockBuilder = this.blockBuilderFactory.create(orchestratorFork);
|
|
352
|
-
await blockBuilder.startNewBlock(newGlobalVariables, l1ToL2Messages, previousBlockHeader);
|
|
353
|
-
// Deadline for processing depends on whether we're proposing a block
|
|
354
|
-
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
355
|
-
const processingEndTimeWithinSlot = opts.validateOnly ? this.timetable.getValidatorReexecTimeEnd(secondsIntoSlot) : this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
|
|
356
|
-
// Deadline is only set if enforceTimeTable is enabled.
|
|
357
|
-
const deadline = this.enforceTimeTable ? new Date((this.getSlotStartTimestamp(slot) + processingEndTimeWithinSlot) * 1000) : undefined;
|
|
358
|
-
this.log.verbose(`Processing pending txs`, {
|
|
359
|
-
slot,
|
|
360
|
-
slotStart: new Date(this.getSlotStartTimestamp(slot) * 1000),
|
|
361
|
-
now: new Date(this.dateProvider.now()),
|
|
362
|
-
deadline
|
|
363
|
-
});
|
|
364
|
-
const validators = createValidatorsForBlockBuilding(publicProcessorFork, this.contractDataSource, newGlobalVariables, this.allowedInSetup);
|
|
365
|
-
// TODO(#11000): Public processor should just handle processing, one tx at a time. It should be responsibility
|
|
366
|
-
// of the sequencer to update world state and iterate over txs. We should refactor this along with unifying the
|
|
367
|
-
// publicProcessorFork and orchestratorFork, to avoid doing tree insertions twice when building the block.
|
|
368
|
-
const proposerLimits = {
|
|
369
|
-
maxTransactions: this.maxTxsPerBlock,
|
|
370
|
-
maxBlockSize: this.maxBlockSizeInBytes,
|
|
371
|
-
maxBlockGas: this.maxBlockGas
|
|
372
|
-
};
|
|
373
|
-
const limits = opts.validateOnly ? {
|
|
374
|
-
deadline
|
|
375
|
-
} : {
|
|
376
|
-
deadline,
|
|
377
|
-
...proposerLimits
|
|
378
|
-
};
|
|
379
|
-
const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(()=>processor.process(pendingTxs, limits, validators));
|
|
380
|
-
if (!opts.validateOnly && failedTxs.length > 0) {
|
|
381
|
-
const failedTxData = failedTxs.map((fail)=>fail.tx);
|
|
382
|
-
const failedTxHashes = await Tx.getHashes(failedTxData);
|
|
383
|
-
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
384
|
-
await this.p2pClient.deleteTxs(failedTxHashes);
|
|
385
|
-
}
|
|
386
|
-
if (!opts.validateOnly && // We check for minTxCount only if we are proposing a block, not if we are validating it
|
|
387
|
-
!this.isFlushing && // And we skip the check when flushing, since we want all pending txs to go out, no matter if too few
|
|
388
|
-
this.minTxsPerBlock !== undefined && processedTxs.length < this.minTxsPerBlock) {
|
|
389
|
-
this.log.warn(`Block ${blockNumber} has too few txs to be proposed (got ${processedTxs.length} but required ${this.minTxsPerBlock})`, {
|
|
390
|
-
slot,
|
|
391
|
-
blockNumber,
|
|
392
|
-
processedTxCount: processedTxs.length
|
|
393
|
-
});
|
|
394
|
-
throw new Error(`Block has too few successful txs to be proposed`);
|
|
395
|
-
}
|
|
396
|
-
const start = process.hrtime.bigint();
|
|
397
|
-
await blockBuilder.addTxs(processedTxs);
|
|
398
|
-
const end = process.hrtime.bigint();
|
|
399
|
-
const duration = Number(end - start) / 1_000;
|
|
400
|
-
this.metrics.recordBlockBuilderTreeInsertions(duration);
|
|
401
|
-
// All real transactions have been added, set the block as full and pad if needed
|
|
402
|
-
const block = await blockBuilder.setBlockCompleted();
|
|
403
|
-
// How much public gas was processed
|
|
404
|
-
const publicGas = processedTxs.reduce((acc, tx)=>acc.add(tx.gasUsed.publicGas), Gas.empty());
|
|
405
|
-
return {
|
|
406
|
-
block,
|
|
407
|
-
publicGas,
|
|
408
|
-
publicProcessorDuration,
|
|
409
|
-
numMsgs: l1ToL2Messages.length,
|
|
410
|
-
numTxs: processedTxs.length,
|
|
411
|
-
numFailedTxs: failedTxs.length,
|
|
412
|
-
blockBuildingTimer
|
|
413
|
-
};
|
|
414
|
-
} finally{
|
|
415
|
-
// We create a fresh processor each time to reset any cached state (eg storage writes)
|
|
416
|
-
// We wait a bit to close the forks since the processor may still be working on a dangling tx
|
|
417
|
-
// which was interrupted due to the processingDeadline being hit.
|
|
418
|
-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
419
|
-
setTimeout(async ()=>{
|
|
420
|
-
try {
|
|
421
|
-
await publicProcessorFork.close();
|
|
422
|
-
await orchestratorFork.close();
|
|
423
|
-
} catch (err) {
|
|
424
|
-
// This can happen if the sequencer is stopped before we hit this timeout.
|
|
425
|
-
this.log.warn(`Error closing forks for block processing`, err);
|
|
426
|
-
}
|
|
427
|
-
}, 5000);
|
|
466
|
+
async dropFailedTxsFromP2P(failedTxs) {
|
|
467
|
+
if (failedTxs.length === 0) {
|
|
468
|
+
return;
|
|
428
469
|
}
|
|
470
|
+
const failedTxData = failedTxs.map((fail)=>fail.tx);
|
|
471
|
+
const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
|
|
472
|
+
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
473
|
+
await this.p2pClient.deleteTxs(failedTxHashes);
|
|
474
|
+
}
|
|
475
|
+
getBlockBuilderOptions(slot) {
|
|
476
|
+
// Deadline for processing depends on whether we're proposing a block
|
|
477
|
+
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
478
|
+
const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
|
|
479
|
+
// Deadline is only set if enforceTimeTable is enabled.
|
|
480
|
+
const deadline = this.enforceTimeTable ? new Date((this.getSlotStartBuildTimestamp(slot) + processingEndTimeWithinSlot) * 1000) : undefined;
|
|
481
|
+
return {
|
|
482
|
+
maxTransactions: this.maxTxsPerBlock,
|
|
483
|
+
maxBlockSize: this.maxBlockSizeInBytes,
|
|
484
|
+
maxBlockGas: this.maxBlockGas,
|
|
485
|
+
maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
|
|
486
|
+
deadline
|
|
487
|
+
};
|
|
429
488
|
}
|
|
430
489
|
/**
|
|
431
490
|
* @notice Build and propose a block to the chain
|
|
@@ -435,24 +494,36 @@ export { SequencerState };
|
|
|
435
494
|
*
|
|
436
495
|
* @param pendingTxs - Iterable of pending transactions to construct the block from
|
|
437
496
|
* @param proposalHeader - The partial header constructed for the proposal
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
497
|
+
* @param newGlobalVariables - The global variables for the new block
|
|
498
|
+
* @param proposerAddress - The address of the proposer
|
|
499
|
+
*/ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock, publisher) {
|
|
500
|
+
await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
501
|
+
const blockNumber = newGlobalVariables.blockNumber;
|
|
502
|
+
const slot = proposalHeader.slotNumber;
|
|
503
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
|
|
444
504
|
const workTimer = new Timer();
|
|
445
505
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
446
506
|
try {
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
507
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(slot);
|
|
508
|
+
const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
|
|
509
|
+
const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
|
|
510
|
+
const blockBuildDuration = workTimer.ms();
|
|
511
|
+
await this.dropFailedTxsFromP2P(failedTxs);
|
|
512
|
+
const minTxsPerBlock = this.minTxsPerBlock;
|
|
513
|
+
if (numTxs < minTxsPerBlock) {
|
|
514
|
+
this.log.warn(`Block ${blockNumber} has too few txs to be proposed (got ${numTxs} but required ${minTxsPerBlock})`, {
|
|
515
|
+
slot,
|
|
516
|
+
blockNumber,
|
|
517
|
+
numTxs
|
|
518
|
+
});
|
|
519
|
+
throw new Error(`Block has too few successful txs to be proposed`);
|
|
520
|
+
}
|
|
450
521
|
// TODO(@PhilWindle) We should probably periodically check for things like another
|
|
451
522
|
// block being published before ours instead of just waiting on our block
|
|
452
|
-
await
|
|
523
|
+
await publisher.validateBlockHeader(block.getCheckpointHeader(), invalidateBlock);
|
|
453
524
|
const blockStats = {
|
|
454
525
|
eventName: 'l2-block-built',
|
|
455
|
-
creator:
|
|
526
|
+
creator: proposerAddress?.toString() ?? publisher.getSenderAddress().toString(),
|
|
456
527
|
duration: workTimer.ms(),
|
|
457
528
|
publicProcessDuration: publicProcessorDuration,
|
|
458
529
|
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
@@ -466,64 +537,137 @@ export { SequencerState };
|
|
|
466
537
|
txHashes,
|
|
467
538
|
...blockStats
|
|
468
539
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
540
|
+
// In fisherman mode, skip attestation collection
|
|
541
|
+
let attestationsAndSigners;
|
|
542
|
+
if (this.config.fishermanMode) {
|
|
543
|
+
this.log.debug('Skipping attestation collection');
|
|
544
|
+
attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
|
|
545
|
+
} else {
|
|
546
|
+
this.log.debug('Collecting attestations');
|
|
547
|
+
attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
|
|
548
|
+
this.log.verbose(`Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`, {
|
|
474
549
|
blockHash,
|
|
475
|
-
blockNumber
|
|
550
|
+
blockNumber,
|
|
551
|
+
slot
|
|
476
552
|
});
|
|
477
553
|
}
|
|
478
|
-
|
|
479
|
-
|
|
554
|
+
// In fisherman mode, skip attestation signing
|
|
555
|
+
const attestationsAndSignersSignature = this.config.fishermanMode || !this.validatorClient ? Signature.empty() : await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress());
|
|
556
|
+
await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
|
|
557
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
558
|
+
return block;
|
|
480
559
|
} catch (err) {
|
|
481
560
|
this.metrics.recordFailedBlock();
|
|
482
561
|
throw err;
|
|
483
562
|
}
|
|
484
563
|
}
|
|
485
|
-
async collectAttestations(block,
|
|
486
|
-
|
|
487
|
-
|
|
564
|
+
async collectAttestations(block, txs, proposerAddress) {
|
|
565
|
+
const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
|
|
566
|
+
// We checked above that the committee is defined, so this should never happen.
|
|
567
|
+
if (!committee) {
|
|
568
|
+
throw new Error('No committee when collecting attestations');
|
|
569
|
+
}
|
|
488
570
|
if (committee.length === 0) {
|
|
489
571
|
this.log.verbose(`Attesting committee is empty`);
|
|
490
|
-
return
|
|
572
|
+
return CommitteeAttestationsAndSigners.empty();
|
|
491
573
|
} else {
|
|
492
574
|
this.log.debug(`Attesting committee length is ${committee.length}`);
|
|
493
575
|
}
|
|
494
576
|
if (!this.validatorClient) {
|
|
495
|
-
|
|
496
|
-
this.log.error(msg);
|
|
497
|
-
throw new Error(msg);
|
|
577
|
+
throw new Error('Missing validator client: Cannot collect attestations');
|
|
498
578
|
}
|
|
499
579
|
const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
|
|
500
|
-
const slotNumber = block.header.globalVariables.slotNumber
|
|
580
|
+
const slotNumber = block.header.globalVariables.slotNumber;
|
|
501
581
|
this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
|
|
502
582
|
this.log.debug('Creating block proposal for validators');
|
|
503
|
-
const
|
|
583
|
+
const blockProposalOptions = {
|
|
584
|
+
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
585
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
586
|
+
};
|
|
587
|
+
const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), block.archive.root, txs, proposerAddress, blockProposalOptions);
|
|
504
588
|
if (!proposal) {
|
|
505
|
-
|
|
506
|
-
|
|
589
|
+
throw new Error(`Failed to create block proposal`);
|
|
590
|
+
}
|
|
591
|
+
if (this.config.skipCollectingAttestations) {
|
|
592
|
+
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
593
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
594
|
+
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
507
595
|
}
|
|
508
596
|
this.log.debug('Broadcasting block proposal to validators');
|
|
509
|
-
this.validatorClient.broadcastBlockProposal(proposal);
|
|
597
|
+
await this.validatorClient.broadcastBlockProposal(proposal);
|
|
510
598
|
const attestationTimeAllowed = this.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_BLOCK) : this.aztecSlotDuration;
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
599
|
+
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
600
|
+
const timer = new Timer();
|
|
601
|
+
let collectedAttestationsCount = 0;
|
|
602
|
+
try {
|
|
603
|
+
const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
|
|
604
|
+
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
605
|
+
collectedAttestationsCount = attestations.length;
|
|
606
|
+
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
607
|
+
const sorted = orderAttestations(attestations, committee);
|
|
608
|
+
// manipulate the attestations if we've been configured to do so
|
|
609
|
+
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
610
|
+
return this.manipulateAttestations(block, epoch, seed, committee, sorted);
|
|
611
|
+
}
|
|
612
|
+
return new CommitteeAttestationsAndSigners(sorted);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (err && err instanceof AttestationTimeoutError) {
|
|
615
|
+
collectedAttestationsCount = err.collectedCount;
|
|
616
|
+
}
|
|
617
|
+
throw err;
|
|
618
|
+
} finally{
|
|
619
|
+
this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/** Breaks the attestations before publishing based on attack configs */ manipulateAttestations(block, epoch, seed, committee, attestations) {
|
|
623
|
+
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
624
|
+
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
625
|
+
const proposerIndex = Number(this.epochCache.computeProposerIndex(block.slot, epoch, seed, BigInt(committee.length)));
|
|
626
|
+
if (this.config.injectFakeAttestation) {
|
|
627
|
+
// Find non-empty attestations that are not from the proposer
|
|
628
|
+
const nonProposerIndices = [];
|
|
629
|
+
for(let i = 0; i < attestations.length; i++){
|
|
630
|
+
if (!attestations[i].signature.isEmpty() && i !== proposerIndex) {
|
|
631
|
+
nonProposerIndices.push(i);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (nonProposerIndices.length > 0) {
|
|
635
|
+
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
636
|
+
this.log.warn(`Injecting fake attestation in block ${block.number} at index ${targetIndex}`);
|
|
637
|
+
unfreeze(attestations[targetIndex]).signature = Signature.random();
|
|
638
|
+
}
|
|
639
|
+
return new CommitteeAttestationsAndSigners(attestations);
|
|
640
|
+
}
|
|
641
|
+
if (this.config.shuffleAttestationOrdering) {
|
|
642
|
+
this.log.warn(`Shuffling attestation ordering in block ${block.number} (proposer index ${proposerIndex})`);
|
|
643
|
+
const shuffled = [
|
|
644
|
+
...attestations
|
|
645
|
+
];
|
|
646
|
+
const [i, j] = [
|
|
647
|
+
(proposerIndex + 1) % shuffled.length,
|
|
648
|
+
(proposerIndex + 2) % shuffled.length
|
|
649
|
+
];
|
|
650
|
+
const valueI = shuffled[i];
|
|
651
|
+
const valueJ = shuffled[j];
|
|
652
|
+
shuffled[i] = valueJ;
|
|
653
|
+
shuffled[j] = valueI;
|
|
654
|
+
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
655
|
+
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
656
|
+
}
|
|
657
|
+
return new CommitteeAttestationsAndSigners(attestations);
|
|
515
658
|
}
|
|
516
659
|
/**
|
|
517
660
|
* Publishes the L2Block to the rollup contract.
|
|
518
661
|
* @param block - The L2Block to be published.
|
|
519
|
-
*/ async enqueuePublishL2Block(block,
|
|
662
|
+
*/ async enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher) {
|
|
520
663
|
// Publishes new block to the network and awaits the tx to be mined
|
|
521
|
-
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber
|
|
664
|
+
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
|
|
522
665
|
// Time out tx at the end of the slot
|
|
523
|
-
const slot = block.header.globalVariables.slotNumber
|
|
524
|
-
const txTimeoutAt = new Date((this.
|
|
525
|
-
const enqueued = await
|
|
526
|
-
txTimeoutAt
|
|
666
|
+
const slot = block.header.globalVariables.slotNumber;
|
|
667
|
+
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
668
|
+
const enqueued = await publisher.enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, {
|
|
669
|
+
txTimeoutAt,
|
|
670
|
+
forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber
|
|
527
671
|
});
|
|
528
672
|
if (!enqueued) {
|
|
529
673
|
throw new Error(`Failed to enqueue publish of block ${block.number}`);
|
|
@@ -532,81 +676,290 @@ export { SequencerState };
|
|
|
532
676
|
/**
|
|
533
677
|
* Returns whether all dependencies have caught up.
|
|
534
678
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
535
|
-
|
|
536
|
-
|
|
679
|
+
*/ async checkSync(args) {
|
|
680
|
+
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
681
|
+
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
682
|
+
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
683
|
+
const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
|
|
684
|
+
const { slot, ts } = args;
|
|
685
|
+
if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
|
|
686
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
687
|
+
slot,
|
|
688
|
+
ts,
|
|
689
|
+
l1Timestamp
|
|
690
|
+
});
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
537
693
|
const syncedBlocks = await Promise.all([
|
|
538
|
-
this.worldState.status().then((
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
};
|
|
543
|
-
}),
|
|
694
|
+
this.worldState.status().then(({ syncSummary })=>({
|
|
695
|
+
number: syncSummary.latestBlockNumber,
|
|
696
|
+
hash: syncSummary.latestBlockHash
|
|
697
|
+
})),
|
|
544
698
|
this.l2BlockSource.getL2Tips().then((t)=>t.latest),
|
|
545
699
|
this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
|
|
546
|
-
this.l1ToL2MessageSource.
|
|
700
|
+
this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
|
|
701
|
+
this.l2BlockSource.getPendingChainValidationStatus()
|
|
547
702
|
]);
|
|
548
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource] = syncedBlocks;
|
|
549
|
-
|
|
550
|
-
// note that the archiver reports undefined hash for the genesis block
|
|
703
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
|
|
704
|
+
// The archiver reports 'undefined' hash for the genesis block
|
|
551
705
|
// because it doesn't have access to world state to compute it (facepalm)
|
|
552
|
-
|
|
553
|
-
// this should change to hashes once p2p client handles reorgs
|
|
554
|
-
// and once we stop pretending that the l1tol2message source is not
|
|
555
|
-
// just the archiver under a different name
|
|
556
|
-
(!l2BlockSource.hash || p2p.hash === l2BlockSource.hash) && l1ToL2MessageSource === l2BlockSource.number;
|
|
557
|
-
this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, {
|
|
558
|
-
worldStateNumber: worldState.number,
|
|
559
|
-
worldStateHash: worldState.hash,
|
|
560
|
-
l2BlockSourceNumber: l2BlockSource.number,
|
|
561
|
-
l2BlockSourceHash: l2BlockSource.hash,
|
|
562
|
-
p2pNumber: p2p.number,
|
|
563
|
-
p2pHash: p2p.hash,
|
|
564
|
-
l1ToL2MessageSourceNumber: l1ToL2MessageSource
|
|
565
|
-
});
|
|
706
|
+
const result = l2BlockSource.hash === undefined ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 : worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
|
|
566
707
|
if (!result) {
|
|
708
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
709
|
+
worldState,
|
|
710
|
+
l2BlockSource,
|
|
711
|
+
p2p,
|
|
712
|
+
l1ToL2MessageSource
|
|
713
|
+
});
|
|
567
714
|
return undefined;
|
|
568
715
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
// this shouldn't really happen because a moment ago we checked that all components were in synch
|
|
573
|
-
return undefined;
|
|
574
|
-
}
|
|
575
|
-
return {
|
|
576
|
-
blockNumber: block.number,
|
|
577
|
-
archive: block.archive.root
|
|
578
|
-
};
|
|
579
|
-
} else {
|
|
716
|
+
// Special case for genesis state
|
|
717
|
+
const blockNumber = worldState.number;
|
|
718
|
+
if (blockNumber < INITIAL_L2_BLOCK_NUM) {
|
|
580
719
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
581
720
|
return {
|
|
582
721
|
blockNumber: INITIAL_L2_BLOCK_NUM - 1,
|
|
583
|
-
archive
|
|
722
|
+
archive,
|
|
723
|
+
l1Timestamp,
|
|
724
|
+
pendingChainValidationStatus
|
|
584
725
|
};
|
|
585
726
|
}
|
|
727
|
+
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
728
|
+
if (!block) {
|
|
729
|
+
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
730
|
+
this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
block,
|
|
735
|
+
blockNumber: block.number,
|
|
736
|
+
archive: block.archive.root,
|
|
737
|
+
l1Timestamp,
|
|
738
|
+
pendingChainValidationStatus
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Enqueues governance and slashing votes with the publisher. Does not block.
|
|
743
|
+
* @param publisher - The publisher to enqueue votes with
|
|
744
|
+
* @param attestorAddress - The attestor address to use for signing
|
|
745
|
+
* @param slot - The slot number
|
|
746
|
+
* @param timestamp - The timestamp for the votes
|
|
747
|
+
* @param context - Optional context for logging (e.g., block number)
|
|
748
|
+
* @returns A tuple of [governanceEnqueued, slashingEnqueued]
|
|
749
|
+
*/ enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, timestamp) {
|
|
750
|
+
try {
|
|
751
|
+
const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
|
|
752
|
+
const enqueueGovernancePromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
|
|
753
|
+
this.log.error(`Error enqueuing governance vote`, err, {
|
|
754
|
+
slot
|
|
755
|
+
});
|
|
756
|
+
return false;
|
|
757
|
+
}) : undefined;
|
|
758
|
+
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>{
|
|
759
|
+
// Record metrics for fisherman mode
|
|
760
|
+
if (this.config.fishermanMode && actions.length > 0) {
|
|
761
|
+
this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
|
|
762
|
+
slot,
|
|
763
|
+
actionCount: actions.length
|
|
764
|
+
});
|
|
765
|
+
this.metrics.recordSlashingAttempt(actions.length);
|
|
766
|
+
}
|
|
767
|
+
// Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
|
|
768
|
+
return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
|
|
769
|
+
}).catch((err)=>{
|
|
770
|
+
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
771
|
+
slot
|
|
772
|
+
});
|
|
773
|
+
return false;
|
|
774
|
+
}) : undefined;
|
|
775
|
+
return [
|
|
776
|
+
enqueueGovernancePromise,
|
|
777
|
+
enqueueSlashingPromise
|
|
778
|
+
];
|
|
779
|
+
} catch (err) {
|
|
780
|
+
this.log.error(`Error enqueueing governance and slashing votes`, err);
|
|
781
|
+
return [
|
|
782
|
+
undefined,
|
|
783
|
+
undefined
|
|
784
|
+
];
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Checks if we are the proposer for the next slot.
|
|
789
|
+
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
790
|
+
*/ async checkCanPropose(slot) {
|
|
791
|
+
let proposer;
|
|
792
|
+
try {
|
|
793
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
|
|
794
|
+
} catch (e) {
|
|
795
|
+
if (e instanceof NoCommitteeError) {
|
|
796
|
+
this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
|
|
797
|
+
return [
|
|
798
|
+
false,
|
|
799
|
+
undefined
|
|
800
|
+
];
|
|
801
|
+
}
|
|
802
|
+
this.log.error(`Error getting proposer for slot ${slot}`, e);
|
|
803
|
+
return [
|
|
804
|
+
false,
|
|
805
|
+
undefined
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
// If proposer is undefined, then the committee is empty and anyone may propose
|
|
809
|
+
if (proposer === undefined) {
|
|
810
|
+
return [
|
|
811
|
+
true,
|
|
812
|
+
undefined
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
// In fisherman mode, just return the current proposer
|
|
816
|
+
if (this.config.fishermanMode) {
|
|
817
|
+
return [
|
|
818
|
+
true,
|
|
819
|
+
proposer
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
const validatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
823
|
+
const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
|
|
824
|
+
if (!weAreProposer) {
|
|
825
|
+
this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
|
|
826
|
+
validatorAddresses,
|
|
827
|
+
proposer
|
|
828
|
+
});
|
|
829
|
+
return [
|
|
830
|
+
false,
|
|
831
|
+
proposer
|
|
832
|
+
];
|
|
833
|
+
}
|
|
834
|
+
return [
|
|
835
|
+
true,
|
|
836
|
+
proposer
|
|
837
|
+
];
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
|
|
841
|
+
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
842
|
+
*/ async tryVoteWhenSyncFails(args) {
|
|
843
|
+
const { slot, ts } = args;
|
|
844
|
+
// Prevent duplicate attempts in the same slot
|
|
845
|
+
if (this.lastSlotForVoteWhenSyncFailed === slot) {
|
|
846
|
+
this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
// Check if we're past the max time for initializing a proposal
|
|
850
|
+
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
851
|
+
const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
|
|
852
|
+
// If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
|
|
853
|
+
// We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
|
|
854
|
+
if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
|
|
855
|
+
this.log.trace(`Not attempting to vote since there is still for block building`, {
|
|
856
|
+
secondsIntoSlot,
|
|
857
|
+
maxAllowedTime
|
|
858
|
+
});
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
|
|
862
|
+
secondsIntoSlot,
|
|
863
|
+
maxAllowedTime
|
|
864
|
+
});
|
|
865
|
+
// Check if we're a proposer or proposal is open
|
|
866
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
867
|
+
if (!canPropose) {
|
|
868
|
+
this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, {
|
|
869
|
+
slot,
|
|
870
|
+
proposer
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
// Mark this slot as attempted
|
|
875
|
+
this.lastSlotForVoteWhenSyncFailed = slot;
|
|
876
|
+
// Get a publisher for voting
|
|
877
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
878
|
+
this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
|
|
879
|
+
attestorAddress,
|
|
880
|
+
slot
|
|
881
|
+
});
|
|
882
|
+
// Enqueue governance and slashing votes using the shared helper method
|
|
883
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
|
|
884
|
+
await Promise.all(votesPromises);
|
|
885
|
+
if (votesPromises.every((p)=>!p)) {
|
|
886
|
+
this.log.debug(`No votes to enqueue for slot ${slot}`);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
this.log.info(`Voting in slot ${slot} despite sync failure`, {
|
|
890
|
+
slot
|
|
891
|
+
});
|
|
892
|
+
await publisher.sendRequests();
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|
|
896
|
+
* has been there without being invalidated and whether the sequencer is in the committee or not. We always
|
|
897
|
+
* have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
|
|
898
|
+
* and if they fail, any sequencer will try as well.
|
|
899
|
+
*/ async considerInvalidatingBlock(syncedTo, currentSlot) {
|
|
900
|
+
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
901
|
+
if (pendingChainValidationStatus.valid) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const { publisher } = await this.publisherFactory.create(undefined);
|
|
905
|
+
const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
|
|
906
|
+
const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
|
|
907
|
+
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
|
|
908
|
+
const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
909
|
+
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
|
|
910
|
+
const logData = {
|
|
911
|
+
invalidL1Timestamp: invalidBlockTimestamp,
|
|
912
|
+
l1Timestamp,
|
|
913
|
+
invalidBlock: pendingChainValidationStatus.block,
|
|
914
|
+
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
915
|
+
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
916
|
+
ourValidatorAddresses,
|
|
917
|
+
currentSlot
|
|
918
|
+
};
|
|
919
|
+
const inCurrentCommittee = ()=>this.epochCache.getCommittee(currentSlot).then((c)=>c?.committee?.some((member)=>ourValidatorAddresses.some((addr)=>addr.equals(member))));
|
|
920
|
+
const invalidateAsCommitteeMember = secondsBeforeInvalidatingBlockAsCommitteeMember !== undefined && secondsBeforeInvalidatingBlockAsCommitteeMember > 0 && timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsCommitteeMember && await inCurrentCommittee();
|
|
921
|
+
const invalidateAsNonCommitteeMember = secondsBeforeInvalidatingBlockAsNonCommitteeMember !== undefined && secondsBeforeInvalidatingBlockAsNonCommitteeMember > 0 && timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsNonCommitteeMember;
|
|
922
|
+
if (!invalidateAsCommitteeMember && !invalidateAsNonCommitteeMember) {
|
|
923
|
+
this.log.debug(`Not invalidating pending chain`, logData);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
927
|
+
if (!invalidateBlock) {
|
|
928
|
+
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
|
|
932
|
+
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
933
|
+
if (!this.config.fishermanMode) {
|
|
934
|
+
await publisher.sendRequests();
|
|
935
|
+
} else {
|
|
936
|
+
this.log.info('Invalidating block in fisherman mode, clearing pending requests');
|
|
937
|
+
publisher.clearPendingRequests();
|
|
938
|
+
}
|
|
586
939
|
}
|
|
587
|
-
|
|
588
|
-
return
|
|
940
|
+
getSlotStartBuildTimestamp(slotNumber) {
|
|
941
|
+
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
|
589
942
|
}
|
|
590
943
|
getSecondsIntoSlot(slotNumber) {
|
|
591
|
-
const slotStartTimestamp = this.
|
|
944
|
+
const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
|
|
592
945
|
return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
|
|
593
946
|
}
|
|
594
947
|
get aztecSlotDuration() {
|
|
595
948
|
return this.l1Constants.slotDuration;
|
|
596
949
|
}
|
|
597
|
-
get
|
|
598
|
-
return this.
|
|
950
|
+
get maxL2BlockGas() {
|
|
951
|
+
return this.config.maxL2BlockGas;
|
|
599
952
|
}
|
|
600
|
-
|
|
601
|
-
return this.
|
|
953
|
+
getSlasherClient() {
|
|
954
|
+
return this.slasherClient;
|
|
602
955
|
}
|
|
603
956
|
}
|
|
604
957
|
_ts_decorate([
|
|
605
958
|
trackSpan('Sequencer.work')
|
|
606
|
-
], Sequencer.prototype, "
|
|
959
|
+
], Sequencer.prototype, "safeWork", null);
|
|
607
960
|
_ts_decorate([
|
|
608
|
-
trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs,
|
|
609
|
-
[Attributes.BLOCK_NUMBER]:
|
|
961
|
+
trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
|
|
962
|
+
[Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber
|
|
610
963
|
}))
|
|
611
964
|
], Sequencer.prototype, "buildBlockAndEnqueuePublish", null);
|
|
612
965
|
_ts_decorate([
|