@aztec/sequencer-client 0.0.0-test.1 → 0.0.1-commit.b655e406
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +25 -25
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +65 -51
- package/dest/config.d.ts +6 -14
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +50 -54
- package/dest/global_variable_builder/global_builder.d.ts +11 -6
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +39 -34
- package/dest/index.d.ts +1 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/publisher/config.d.ts +6 -8
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +19 -17
- package/dest/publisher/index.d.ts +2 -0
- 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 +2 -1
- 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 +102 -69
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +606 -212
- 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 +130 -0
- package/dest/sequencer/config.d.ts +5 -0
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +11 -0
- package/dest/sequencer/errors.d.ts.map +1 -0
- package/dest/sequencer/errors.js +15 -0
- package/dest/sequencer/index.d.ts +1 -1
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +1 -1
- package/dest/sequencer/metrics.d.ts +18 -11
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +84 -50
- package/dest/sequencer/sequencer.d.ts +120 -81
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +589 -359
- package/dest/sequencer/timetable.d.ts +32 -20
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +57 -30
- package/dest/sequencer/utils.d.ts +11 -35
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +9 -47
- package/dest/test/index.d.ts +7 -0
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +0 -4
- package/dest/tx_validator/nullifier_cache.d.ts +0 -2
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +9 -10
- 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 +42 -43
- package/src/client/sequencer-client.ts +94 -84
- package/src/config.ts +57 -61
- package/src/global_variable_builder/global_builder.ts +44 -23
- package/src/index.ts +6 -2
- package/src/publisher/config.ts +26 -24
- package/src/publisher/index.ts +4 -0
- package/src/publisher/sequencer-publisher-factory.ts +90 -0
- package/src/publisher/sequencer-publisher-metrics.ts +24 -2
- package/src/publisher/sequencer-publisher.ts +729 -235
- package/src/sequencer/block_builder.ts +218 -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 +109 -55
- package/src/sequencer/sequencer.ts +766 -415
- 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_BLOCK, 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 } 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,63 @@ 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;
|
|
66
72
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
|
|
67
73
|
enforceTimeTable;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
this.contractDataSource = contractDataSource;
|
|
80
|
-
this.l1Constants = l1Constants;
|
|
81
|
-
this.dateProvider = dateProvider;
|
|
82
|
-
this.config = 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));
|
|
74
|
+
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
75
|
+
// But we have a number of tests that abuse/rely on this class having a permanent publisher.
|
|
76
|
+
// As long as those tests only configure a single publisher they will continue to work.
|
|
77
|
+
// This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
|
|
78
|
+
// for the block proposer.
|
|
79
|
+
publisher;
|
|
80
|
+
constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
|
|
81
|
+
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;
|
|
82
|
+
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
83
|
+
// Initialize config
|
|
84
|
+
this.updateConfig(this.config);
|
|
101
85
|
}
|
|
102
86
|
get tracer() {
|
|
103
87
|
return this.metrics.tracer;
|
|
104
88
|
}
|
|
89
|
+
getValidatorAddresses() {
|
|
90
|
+
return this.validatorClient?.getValidatorAddresses();
|
|
91
|
+
}
|
|
92
|
+
getConfig() {
|
|
93
|
+
return this.config;
|
|
94
|
+
}
|
|
105
95
|
/**
|
|
106
|
-
* Updates sequencer config.
|
|
96
|
+
* Updates sequencer config by the defined values in the config on input.
|
|
107
97
|
* @param config - New parameters.
|
|
108
|
-
*/
|
|
109
|
-
this.log.info(`Sequencer config set`, omit(pickFromSchema(config, SequencerConfigSchema), '
|
|
98
|
+
*/ updateConfig(config) {
|
|
99
|
+
this.log.info(`Sequencer config set`, omit(pickFromSchema(config, SequencerConfigSchema), 'txPublicSetupAllowList'));
|
|
110
100
|
if (config.transactionPollingIntervalMS !== undefined) {
|
|
111
101
|
this.pollingIntervalMs = config.transactionPollingIntervalMS;
|
|
112
102
|
}
|
|
@@ -122,22 +112,11 @@ export { SequencerState };
|
|
|
122
112
|
if (config.maxL2BlockGas !== undefined) {
|
|
123
113
|
this.maxBlockGas = new Gas(this.maxBlockGas.daGas, config.maxL2BlockGas);
|
|
124
114
|
}
|
|
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
115
|
if (config.maxBlockSizeInBytes !== undefined) {
|
|
137
116
|
this.maxBlockSizeInBytes = config.maxBlockSizeInBytes;
|
|
138
117
|
}
|
|
139
118
|
if (config.governanceProposerPayload) {
|
|
140
|
-
this.
|
|
119
|
+
this.governanceProposerPayload = config.governanceProposerPayload;
|
|
141
120
|
}
|
|
142
121
|
if (config.maxL1TxInclusionTimeIntoSlot !== undefined) {
|
|
143
122
|
this.maxL1TxInclusionTimeIntoSlot = config.maxL1TxInclusionTimeIntoSlot;
|
|
@@ -147,43 +126,46 @@ export { SequencerState };
|
|
|
147
126
|
}
|
|
148
127
|
this.setTimeTable();
|
|
149
128
|
// TODO: Just read everything from the config object as needed instead of copying everything into local vars.
|
|
150
|
-
this.config
|
|
129
|
+
// Update all values on this.config that are populated in the config object.
|
|
130
|
+
Object.assign(this.config, config);
|
|
151
131
|
}
|
|
152
132
|
setTimeTable() {
|
|
153
|
-
this.timetable = new SequencerTimetable(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
133
|
+
this.timetable = new SequencerTimetable({
|
|
134
|
+
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
135
|
+
aztecSlotDuration: this.aztecSlotDuration,
|
|
136
|
+
maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
|
|
137
|
+
attestationPropagationTime: this.config.attestationPropagationTime,
|
|
138
|
+
enforce: this.enforceTimeTable
|
|
139
|
+
}, this.metrics, this.log);
|
|
140
|
+
}
|
|
141
|
+
async init() {
|
|
142
|
+
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
157
143
|
}
|
|
158
144
|
/**
|
|
159
145
|
* Starts the sequencer and moves to IDLE state.
|
|
160
|
-
*/
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
|
|
146
|
+
*/ start() {
|
|
147
|
+
this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
|
|
148
|
+
this.setState(SequencerState.IDLE, undefined, {
|
|
149
|
+
force: true
|
|
150
|
+
});
|
|
164
151
|
this.runningPromise.start();
|
|
165
|
-
this.log.info(
|
|
152
|
+
this.log.info('Started sequencer');
|
|
166
153
|
}
|
|
167
154
|
/**
|
|
168
155
|
* Stops the sequencer from processing txs and moves to STOPPED state.
|
|
169
156
|
*/ async stop() {
|
|
170
|
-
this.log.
|
|
171
|
-
|
|
157
|
+
this.log.info(`Stopping sequencer`);
|
|
158
|
+
this.setState(SequencerState.STOPPING, undefined, {
|
|
159
|
+
force: true
|
|
160
|
+
});
|
|
161
|
+
this.publisher?.interrupt();
|
|
172
162
|
await this.runningPromise?.stop();
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
this.setState(SequencerState.STOPPED, undefined, {
|
|
164
|
+
force: true
|
|
165
|
+
});
|
|
176
166
|
this.log.info('Stopped sequencer');
|
|
177
167
|
}
|
|
178
168
|
/**
|
|
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
169
|
* Returns the current state of the sequencer.
|
|
188
170
|
* @returns An object with a state entry with one of SequencerState.
|
|
189
171
|
*/ status() {
|
|
@@ -191,9 +173,6 @@ export { SequencerState };
|
|
|
191
173
|
state: this.state
|
|
192
174
|
};
|
|
193
175
|
}
|
|
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
176
|
/**
|
|
198
177
|
* @notice Performs most of the sequencer duties:
|
|
199
178
|
* - Checks if we are up to date
|
|
@@ -201,231 +180,250 @@ export { SequencerState };
|
|
|
201
180
|
* - Collect attestations for the block
|
|
202
181
|
* - Submit block
|
|
203
182
|
* - If our block for some reason is not included, revert the state
|
|
204
|
-
*/ async
|
|
205
|
-
this.setState(SequencerState.SYNCHRONIZING,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
183
|
+
*/ async work() {
|
|
184
|
+
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
185
|
+
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
186
|
+
// Check we have not already published a block for this slot (cheapest check)
|
|
187
|
+
if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
|
|
188
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
192
|
+
const syncedTo = await this.checkSync({
|
|
193
|
+
ts,
|
|
194
|
+
slot
|
|
195
|
+
});
|
|
196
|
+
if (!syncedTo) {
|
|
197
|
+
await this.tryVoteWhenSyncFails({
|
|
198
|
+
slot,
|
|
199
|
+
ts
|
|
200
|
+
});
|
|
210
201
|
return;
|
|
211
202
|
}
|
|
212
|
-
|
|
213
|
-
const newBlockNumber =
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
203
|
+
const chainTipArchive = syncedTo.archive;
|
|
204
|
+
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
205
|
+
const syncLogData = {
|
|
206
|
+
now,
|
|
207
|
+
syncedToL1Ts: syncedTo.l1Timestamp,
|
|
208
|
+
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
209
|
+
nextL2Slot: slot,
|
|
210
|
+
nextL2SlotTs: ts,
|
|
211
|
+
l1SlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
212
|
+
newBlockNumber,
|
|
213
|
+
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
|
|
214
|
+
};
|
|
215
|
+
// Check that we are a proposer for the next slot
|
|
216
|
+
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
217
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
218
|
+
// If we are not a proposer, check if we should invalidate a invalid block, and bail
|
|
219
|
+
if (!canPropose) {
|
|
220
|
+
await this.considerInvalidatingBlock(syncedTo, slot);
|
|
219
221
|
return;
|
|
220
222
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
224
|
+
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
225
|
+
this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
|
|
226
|
+
...syncLogData,
|
|
227
|
+
block: syncedTo.block.header.toInspect()
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// We now need to get ourselves a publisher.
|
|
232
|
+
// The returned attestor will be the one we provided if we provided one.
|
|
233
|
+
// Otherwise it will be a valid attestor for the returned publisher.
|
|
234
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
235
|
+
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
236
|
+
this.publisher = publisher;
|
|
237
|
+
const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
|
|
238
|
+
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
|
|
239
|
+
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
240
|
+
const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
|
|
241
|
+
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
242
|
+
// if all the previous checks are good, but we do it just in case.
|
|
243
|
+
const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposer ?? EthAddress.ZERO, invalidateBlock);
|
|
244
|
+
if (canProposeCheck === undefined) {
|
|
245
|
+
this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
|
|
246
|
+
this.emit('proposer-rollup-check-failed', {
|
|
247
|
+
reason: 'Rollup contract check failed'
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
} else if (canProposeCheck.slot !== slot) {
|
|
251
|
+
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}.`, {
|
|
252
|
+
...syncLogData,
|
|
253
|
+
rollup: canProposeCheck,
|
|
254
|
+
newBlockNumber,
|
|
255
|
+
expectedSlot: slot
|
|
256
|
+
});
|
|
257
|
+
this.emit('proposer-rollup-check-failed', {
|
|
258
|
+
reason: 'Slot mismatch'
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
} else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
|
|
262
|
+
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.blockNumber}.`, {
|
|
263
|
+
...syncLogData,
|
|
264
|
+
rollup: canProposeCheck,
|
|
265
|
+
newBlockNumber,
|
|
266
|
+
expectedSlot: slot
|
|
267
|
+
});
|
|
268
|
+
this.emit('proposer-rollup-check-failed', {
|
|
269
|
+
reason: 'Block mismatch'
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
|
|
274
|
+
...syncLogData
|
|
275
|
+
});
|
|
276
|
+
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
|
|
277
|
+
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
278
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
|
|
279
|
+
// Enqueues block invalidation
|
|
280
|
+
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
281
|
+
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
282
|
+
}
|
|
283
|
+
// Actual block building
|
|
225
284
|
this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
|
|
285
|
+
const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
|
|
286
|
+
// Wait until the voting promises have resolved, so all requests are enqueued
|
|
287
|
+
await Promise.all(votesPromises);
|
|
288
|
+
// And send the tx to L1
|
|
289
|
+
const l1Response = await publisher.sendRequests();
|
|
290
|
+
const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
|
|
291
|
+
if (proposedBlock) {
|
|
292
|
+
this.lastBlockPublished = block;
|
|
293
|
+
this.emit('block-published', {
|
|
294
|
+
blockNumber: newBlockNumber,
|
|
295
|
+
slot: Number(slot)
|
|
296
|
+
});
|
|
297
|
+
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
298
|
+
} else if (block) {
|
|
299
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
300
|
+
}
|
|
301
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
302
|
+
}
|
|
303
|
+
/** Tries building a block proposal, and if successful, enqueues it for publishing. */ async tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock) {
|
|
226
304
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
305
|
+
proposer,
|
|
306
|
+
publisher: publisher.getSenderAddress(),
|
|
307
|
+
globalVariables: newGlobalVariables.toInspect(),
|
|
227
308
|
chainTipArchive,
|
|
228
309
|
blockNumber: newBlockNumber,
|
|
229
310
|
slot
|
|
230
311
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
312
|
+
const proposalHeader = CheckpointHeader.from({
|
|
313
|
+
...newGlobalVariables,
|
|
314
|
+
timestamp: newGlobalVariables.timestamp,
|
|
315
|
+
lastArchiveRoot: chainTipArchive,
|
|
316
|
+
contentCommitment: ContentCommitment.empty(),
|
|
317
|
+
totalManaUsed: Fr.ZERO
|
|
318
|
+
});
|
|
319
|
+
let block;
|
|
234
320
|
const pendingTxCount = await this.p2pClient.getPendingTxCount();
|
|
235
|
-
if (pendingTxCount >= this.minTxsPerBlock
|
|
321
|
+
if (pendingTxCount >= this.minTxsPerBlock) {
|
|
236
322
|
// We don't fetch exactly maxTxsPerBlock txs here because we may not need all of them if we hit a limit before,
|
|
237
323
|
// and also we may need to fetch more if we don't have enough valid txs.
|
|
238
324
|
const pendingTxs = this.p2pClient.iteratePendingTxs();
|
|
239
|
-
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
|
|
325
|
+
try {
|
|
326
|
+
block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
this.emit('block-build-failed', {
|
|
329
|
+
reason: err.message
|
|
243
330
|
});
|
|
244
|
-
|
|
245
|
-
|
|
331
|
+
if (err instanceof FormattedViemError) {
|
|
332
|
+
this.log.verbose(`Unable to build/enqueue block ${err.message}`);
|
|
333
|
+
} else {
|
|
334
|
+
this.log.error(`Error building/enqueuing block`, err, {
|
|
335
|
+
blockNumber: newBlockNumber,
|
|
336
|
+
slot
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
246
340
|
} else {
|
|
247
|
-
this.log.
|
|
248
|
-
|
|
249
|
-
await enqueueGovernanceVotePromise.catch((err)=>{
|
|
250
|
-
this.log.error(`Error enqueuing governance vote`, err, {
|
|
341
|
+
this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
|
|
342
|
+
chainTipArchive,
|
|
251
343
|
blockNumber: newBlockNumber,
|
|
252
344
|
slot
|
|
253
345
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
blockNumber: newBlockNumber,
|
|
258
|
-
slot
|
|
346
|
+
this.emit('tx-count-check-failed', {
|
|
347
|
+
minTxs: this.minTxsPerBlock,
|
|
348
|
+
availableTxs: pendingTxCount
|
|
259
349
|
});
|
|
260
|
-
});
|
|
261
|
-
await this.publisher.sendRequests();
|
|
262
|
-
if (finishedFlushing) {
|
|
263
|
-
this.isFlushing = false;
|
|
264
350
|
}
|
|
265
|
-
|
|
351
|
+
return block;
|
|
266
352
|
}
|
|
267
|
-
async
|
|
353
|
+
async safeWork() {
|
|
268
354
|
try {
|
|
269
|
-
await this.
|
|
355
|
+
await this.work();
|
|
270
356
|
} catch (err) {
|
|
271
357
|
if (err instanceof SequencerTooSlowError) {
|
|
272
|
-
|
|
358
|
+
// Log as warn only if we had to abort halfway through the block proposal
|
|
359
|
+
const logLvl = [
|
|
360
|
+
SequencerState.INITIALIZING_PROPOSAL,
|
|
361
|
+
SequencerState.PROPOSER_CHECK
|
|
362
|
+
].includes(err.proposedState) ? 'debug' : 'warn';
|
|
363
|
+
this.log[logLvl](err.message, {
|
|
364
|
+
now: this.dateProvider.nowInSeconds()
|
|
365
|
+
});
|
|
273
366
|
} else {
|
|
274
367
|
// Re-throw other errors
|
|
275
368
|
throw err;
|
|
276
369
|
}
|
|
277
370
|
} finally{
|
|
278
|
-
this.setState(SequencerState.IDLE,
|
|
371
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
279
372
|
}
|
|
280
373
|
}
|
|
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;
|
|
293
|
-
}
|
|
294
|
-
const [slot, blockNumber] = result;
|
|
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);
|
|
374
|
+
setState(proposedState, slotNumber, opts = {}) {
|
|
375
|
+
if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
|
|
376
|
+
this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
|
|
377
|
+
throw new SequencerInterruptedError();
|
|
299
378
|
}
|
|
300
|
-
|
|
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) {
|
|
379
|
+
if (this.state === SequencerState.STOPPED && !opts.force) {
|
|
312
380
|
this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
|
|
313
381
|
return;
|
|
314
382
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
383
|
+
let secondsIntoSlot = undefined;
|
|
384
|
+
if (slotNumber !== undefined) {
|
|
385
|
+
secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
|
|
386
|
+
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
387
|
+
}
|
|
388
|
+
const boringStates = [
|
|
389
|
+
SequencerState.IDLE,
|
|
390
|
+
SequencerState.SYNCHRONIZING
|
|
391
|
+
];
|
|
392
|
+
const logLevel = boringStates.includes(proposedState) && boringStates.includes(this.state) ? 'trace' : 'debug';
|
|
393
|
+
this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, {
|
|
394
|
+
slotNumber,
|
|
395
|
+
secondsIntoSlot
|
|
396
|
+
});
|
|
397
|
+
this.emit('state-changed', {
|
|
398
|
+
oldState: this.state,
|
|
399
|
+
newState: proposedState,
|
|
400
|
+
secondsIntoSlot,
|
|
401
|
+
slotNumber
|
|
402
|
+
});
|
|
318
403
|
this.state = proposedState;
|
|
319
404
|
}
|
|
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);
|
|
405
|
+
async dropFailedTxsFromP2P(failedTxs) {
|
|
406
|
+
if (failedTxs.length === 0) {
|
|
407
|
+
return;
|
|
428
408
|
}
|
|
409
|
+
const failedTxData = failedTxs.map((fail)=>fail.tx);
|
|
410
|
+
const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
|
|
411
|
+
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
412
|
+
await this.p2pClient.deleteTxs(failedTxHashes);
|
|
413
|
+
}
|
|
414
|
+
getBlockBuilderOptions(slot) {
|
|
415
|
+
// Deadline for processing depends on whether we're proposing a block
|
|
416
|
+
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
417
|
+
const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
|
|
418
|
+
// Deadline is only set if enforceTimeTable is enabled.
|
|
419
|
+
const deadline = this.enforceTimeTable ? new Date((this.getSlotStartBuildTimestamp(slot) + processingEndTimeWithinSlot) * 1000) : undefined;
|
|
420
|
+
return {
|
|
421
|
+
maxTransactions: this.maxTxsPerBlock,
|
|
422
|
+
maxBlockSize: this.maxBlockSizeInBytes,
|
|
423
|
+
maxBlockGas: this.maxBlockGas,
|
|
424
|
+
maxBlobFields: BLOBS_PER_BLOCK * FIELDS_PER_BLOB,
|
|
425
|
+
deadline
|
|
426
|
+
};
|
|
429
427
|
}
|
|
430
428
|
/**
|
|
431
429
|
* @notice Build and propose a block to the chain
|
|
@@ -435,24 +433,36 @@ export { SequencerState };
|
|
|
435
433
|
*
|
|
436
434
|
* @param pendingTxs - Iterable of pending transactions to construct the block from
|
|
437
435
|
* @param proposalHeader - The partial header constructed for the proposal
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
436
|
+
* @param newGlobalVariables - The global variables for the new block
|
|
437
|
+
* @param proposerAddress - The address of the proposer
|
|
438
|
+
*/ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock, publisher) {
|
|
439
|
+
await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
440
|
+
const blockNumber = newGlobalVariables.blockNumber;
|
|
441
|
+
const slot = proposalHeader.slotNumber.toBigInt();
|
|
442
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
|
|
444
443
|
const workTimer = new Timer();
|
|
445
444
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
446
445
|
try {
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
446
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
|
|
447
|
+
const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
|
|
448
|
+
const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
|
|
449
|
+
const blockBuildDuration = workTimer.ms();
|
|
450
|
+
await this.dropFailedTxsFromP2P(failedTxs);
|
|
451
|
+
const minTxsPerBlock = this.minTxsPerBlock;
|
|
452
|
+
if (numTxs < minTxsPerBlock) {
|
|
453
|
+
this.log.warn(`Block ${blockNumber} has too few txs to be proposed (got ${numTxs} but required ${minTxsPerBlock})`, {
|
|
454
|
+
slot,
|
|
455
|
+
blockNumber,
|
|
456
|
+
numTxs
|
|
457
|
+
});
|
|
458
|
+
throw new Error(`Block has too few successful txs to be proposed`);
|
|
459
|
+
}
|
|
450
460
|
// TODO(@PhilWindle) We should probably periodically check for things like another
|
|
451
461
|
// block being published before ours instead of just waiting on our block
|
|
452
|
-
await
|
|
462
|
+
await publisher.validateBlockHeader(block.getCheckpointHeader(), invalidateBlock);
|
|
453
463
|
const blockStats = {
|
|
454
464
|
eventName: 'l2-block-built',
|
|
455
|
-
creator:
|
|
465
|
+
creator: proposerAddress?.toString() ?? publisher.getSenderAddress().toString(),
|
|
456
466
|
duration: workTimer.ms(),
|
|
457
467
|
publicProcessDuration: publicProcessorDuration,
|
|
458
468
|
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
@@ -467,24 +477,29 @@ export { SequencerState };
|
|
|
467
477
|
...blockStats
|
|
468
478
|
});
|
|
469
479
|
this.log.debug('Collecting attestations');
|
|
470
|
-
const
|
|
471
|
-
const attestations = await this.collectAttestations(block, txHashes);
|
|
480
|
+
const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
|
|
472
481
|
if (attestations !== undefined) {
|
|
473
482
|
this.log.verbose(`Collected ${attestations.length} attestations`, {
|
|
474
483
|
blockHash,
|
|
475
484
|
blockNumber
|
|
476
485
|
});
|
|
477
486
|
}
|
|
478
|
-
|
|
479
|
-
|
|
487
|
+
const attestationsAndSigners = new CommitteeAttestationsAndSigners(attestations ?? []);
|
|
488
|
+
const attestationsAndSignersSignature = this.validatorClient ? await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress()) : Signature.empty();
|
|
489
|
+
await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
|
|
490
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
491
|
+
return block;
|
|
480
492
|
} catch (err) {
|
|
481
493
|
this.metrics.recordFailedBlock();
|
|
482
494
|
throw err;
|
|
483
495
|
}
|
|
484
496
|
}
|
|
485
|
-
async collectAttestations(block,
|
|
486
|
-
|
|
487
|
-
|
|
497
|
+
async collectAttestations(block, txs, proposerAddress) {
|
|
498
|
+
const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
|
|
499
|
+
// We checked above that the committee is defined, so this should never happen.
|
|
500
|
+
if (!committee) {
|
|
501
|
+
throw new Error('No committee when collecting attestations');
|
|
502
|
+
}
|
|
488
503
|
if (committee.length === 0) {
|
|
489
504
|
this.log.verbose(`Attesting committee is empty`);
|
|
490
505
|
return undefined;
|
|
@@ -500,30 +515,59 @@ export { SequencerState };
|
|
|
500
515
|
const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
|
|
501
516
|
this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
|
|
502
517
|
this.log.debug('Creating block proposal for validators');
|
|
503
|
-
const
|
|
518
|
+
const blockProposalOptions = {
|
|
519
|
+
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
520
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
521
|
+
};
|
|
522
|
+
const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), block.archive.root, block.header.state, txs, proposerAddress, blockProposalOptions);
|
|
504
523
|
if (!proposal) {
|
|
505
|
-
|
|
506
|
-
|
|
524
|
+
throw new Error(`Failed to create block proposal`);
|
|
525
|
+
}
|
|
526
|
+
if (this.config.skipCollectingAttestations) {
|
|
527
|
+
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
528
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
529
|
+
return orderAttestations(attestations ?? [], committee);
|
|
507
530
|
}
|
|
508
531
|
this.log.debug('Broadcasting block proposal to validators');
|
|
509
|
-
this.validatorClient.broadcastBlockProposal(proposal);
|
|
532
|
+
await this.validatorClient.broadcastBlockProposal(proposal);
|
|
510
533
|
const attestationTimeAllowed = this.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_BLOCK) : this.aztecSlotDuration;
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
534
|
+
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
535
|
+
const timer = new Timer();
|
|
536
|
+
let collectedAttestationsCount = 0;
|
|
537
|
+
try {
|
|
538
|
+
const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
|
|
539
|
+
const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
|
|
540
|
+
collectedAttestationsCount = attestations.length;
|
|
541
|
+
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
542
|
+
const sorted = orderAttestations(attestations, committee);
|
|
543
|
+
if (this.config.injectFakeAttestation) {
|
|
544
|
+
const nonEmpty = sorted.filter((a)=>!a.signature.isEmpty());
|
|
545
|
+
const randomIndex = randomInt(nonEmpty.length);
|
|
546
|
+
this.log.warn(`Injecting fake attestation in block ${block.number}`);
|
|
547
|
+
unfreeze(nonEmpty[randomIndex]).signature = Signature.random();
|
|
548
|
+
}
|
|
549
|
+
return sorted;
|
|
550
|
+
} catch (err) {
|
|
551
|
+
if (err && err instanceof AttestationTimeoutError) {
|
|
552
|
+
collectedAttestationsCount = err.collectedCount;
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
} finally{
|
|
556
|
+
this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
|
|
557
|
+
}
|
|
515
558
|
}
|
|
516
559
|
/**
|
|
517
560
|
* Publishes the L2Block to the rollup contract.
|
|
518
561
|
* @param block - The L2Block to be published.
|
|
519
|
-
*/ async enqueuePublishL2Block(block,
|
|
562
|
+
*/ async enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher) {
|
|
520
563
|
// Publishes new block to the network and awaits the tx to be mined
|
|
521
564
|
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
|
|
522
565
|
// Time out tx at the end of the slot
|
|
523
566
|
const slot = block.header.globalVariables.slotNumber.toNumber();
|
|
524
|
-
const txTimeoutAt = new Date((this.
|
|
525
|
-
const enqueued = await
|
|
526
|
-
txTimeoutAt
|
|
567
|
+
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
568
|
+
const enqueued = await publisher.enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, {
|
|
569
|
+
txTimeoutAt,
|
|
570
|
+
forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber
|
|
527
571
|
});
|
|
528
572
|
if (!enqueued) {
|
|
529
573
|
throw new Error(`Failed to enqueue publish of block ${block.number}`);
|
|
@@ -532,81 +576,267 @@ export { SequencerState };
|
|
|
532
576
|
/**
|
|
533
577
|
* Returns whether all dependencies have caught up.
|
|
534
578
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
535
|
-
|
|
536
|
-
|
|
579
|
+
*/ async checkSync(args) {
|
|
580
|
+
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
581
|
+
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
582
|
+
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
583
|
+
const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
|
|
584
|
+
const { slot, ts } = args;
|
|
585
|
+
if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
|
|
586
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
587
|
+
slot,
|
|
588
|
+
ts,
|
|
589
|
+
l1Timestamp
|
|
590
|
+
});
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
537
593
|
const syncedBlocks = await Promise.all([
|
|
538
|
-
this.worldState.status().then((
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
};
|
|
543
|
-
}),
|
|
594
|
+
this.worldState.status().then(({ syncSummary })=>({
|
|
595
|
+
number: syncSummary.latestBlockNumber,
|
|
596
|
+
hash: syncSummary.latestBlockHash
|
|
597
|
+
})),
|
|
544
598
|
this.l2BlockSource.getL2Tips().then((t)=>t.latest),
|
|
545
599
|
this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
|
|
546
|
-
this.l1ToL2MessageSource.
|
|
600
|
+
this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
|
|
601
|
+
this.l2BlockSource.getPendingChainValidationStatus()
|
|
547
602
|
]);
|
|
548
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource] = syncedBlocks;
|
|
549
|
-
|
|
550
|
-
// note that the archiver reports undefined hash for the genesis block
|
|
603
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
|
|
604
|
+
// The archiver reports 'undefined' hash for the genesis block
|
|
551
605
|
// 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
|
-
});
|
|
606
|
+
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
607
|
if (!result) {
|
|
608
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
609
|
+
worldState,
|
|
610
|
+
l2BlockSource,
|
|
611
|
+
p2p,
|
|
612
|
+
l1ToL2MessageSource
|
|
613
|
+
});
|
|
567
614
|
return undefined;
|
|
568
615
|
}
|
|
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 {
|
|
616
|
+
// Special case for genesis state
|
|
617
|
+
const blockNumber = worldState.number;
|
|
618
|
+
if (blockNumber < INITIAL_L2_BLOCK_NUM) {
|
|
580
619
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
581
620
|
return {
|
|
582
621
|
blockNumber: INITIAL_L2_BLOCK_NUM - 1,
|
|
583
|
-
archive
|
|
622
|
+
archive,
|
|
623
|
+
l1Timestamp,
|
|
624
|
+
pendingChainValidationStatus
|
|
584
625
|
};
|
|
585
626
|
}
|
|
627
|
+
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
628
|
+
if (!block) {
|
|
629
|
+
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
630
|
+
this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
block,
|
|
635
|
+
blockNumber: block.number,
|
|
636
|
+
archive: block.archive.root,
|
|
637
|
+
l1Timestamp,
|
|
638
|
+
pendingChainValidationStatus
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Enqueues governance and slashing votes with the publisher. Does not block.
|
|
643
|
+
* @param publisher - The publisher to enqueue votes with
|
|
644
|
+
* @param attestorAddress - The attestor address to use for signing
|
|
645
|
+
* @param slot - The slot number
|
|
646
|
+
* @param timestamp - The timestamp for the votes
|
|
647
|
+
* @param context - Optional context for logging (e.g., block number)
|
|
648
|
+
* @returns A tuple of [governanceEnqueued, slashingEnqueued]
|
|
649
|
+
*/ enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, timestamp) {
|
|
650
|
+
try {
|
|
651
|
+
const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
|
|
652
|
+
const enqueueGovernancePromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
|
|
653
|
+
this.log.error(`Error enqueuing governance vote`, err, {
|
|
654
|
+
slot
|
|
655
|
+
});
|
|
656
|
+
return false;
|
|
657
|
+
}) : undefined;
|
|
658
|
+
const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
|
|
659
|
+
this.log.error(`Error enqueuing slashing actions`, err, {
|
|
660
|
+
slot
|
|
661
|
+
});
|
|
662
|
+
return false;
|
|
663
|
+
}) : undefined;
|
|
664
|
+
return [
|
|
665
|
+
enqueueGovernancePromise,
|
|
666
|
+
enqueueSlashingPromise
|
|
667
|
+
];
|
|
668
|
+
} catch (err) {
|
|
669
|
+
this.log.error(`Error enqueueing governance and slashing votes`, err);
|
|
670
|
+
return [
|
|
671
|
+
undefined,
|
|
672
|
+
undefined
|
|
673
|
+
];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Checks if we are the proposer for the next slot.
|
|
678
|
+
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
679
|
+
*/ async checkCanPropose(slot) {
|
|
680
|
+
let proposer;
|
|
681
|
+
try {
|
|
682
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
|
|
683
|
+
} catch (e) {
|
|
684
|
+
if (e instanceof NoCommitteeError) {
|
|
685
|
+
this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
|
|
686
|
+
return [
|
|
687
|
+
false,
|
|
688
|
+
undefined
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
this.log.error(`Error getting proposer for slot ${slot}`, e);
|
|
692
|
+
return [
|
|
693
|
+
false,
|
|
694
|
+
undefined
|
|
695
|
+
];
|
|
696
|
+
}
|
|
697
|
+
// If proposer is undefined, then the committee is empty and anyone may propose
|
|
698
|
+
if (proposer === undefined) {
|
|
699
|
+
return [
|
|
700
|
+
true,
|
|
701
|
+
undefined
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
const validatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
705
|
+
const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
|
|
706
|
+
if (!weAreProposer) {
|
|
707
|
+
this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
|
|
708
|
+
validatorAddresses,
|
|
709
|
+
proposer
|
|
710
|
+
});
|
|
711
|
+
return [
|
|
712
|
+
false,
|
|
713
|
+
proposer
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
return [
|
|
717
|
+
true,
|
|
718
|
+
proposer
|
|
719
|
+
];
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
|
|
723
|
+
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
724
|
+
*/ async tryVoteWhenSyncFails(args) {
|
|
725
|
+
const { slot, ts } = args;
|
|
726
|
+
// Prevent duplicate attempts in the same slot
|
|
727
|
+
if (this.lastSlotForVoteWhenSyncFailed === slot) {
|
|
728
|
+
this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// Check if we're past the max time for initializing a proposal
|
|
732
|
+
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
733
|
+
const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
|
|
734
|
+
// If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
|
|
735
|
+
// We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
|
|
736
|
+
if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
|
|
737
|
+
this.log.trace(`Not attempting to vote since there is still for block building`, {
|
|
738
|
+
secondsIntoSlot,
|
|
739
|
+
maxAllowedTime
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
|
|
744
|
+
secondsIntoSlot,
|
|
745
|
+
maxAllowedTime
|
|
746
|
+
});
|
|
747
|
+
// Check if we're a proposer or proposal is open
|
|
748
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
749
|
+
if (!canPropose) {
|
|
750
|
+
this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, {
|
|
751
|
+
slot,
|
|
752
|
+
proposer
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// Mark this slot as attempted
|
|
757
|
+
this.lastSlotForVoteWhenSyncFailed = slot;
|
|
758
|
+
// Get a publisher for voting
|
|
759
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
760
|
+
this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
|
|
761
|
+
attestorAddress,
|
|
762
|
+
slot
|
|
763
|
+
});
|
|
764
|
+
// Enqueue governance and slashing votes using the shared helper method
|
|
765
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
|
|
766
|
+
await Promise.all(votesPromises);
|
|
767
|
+
if (votesPromises.every((p)=>!p)) {
|
|
768
|
+
this.log.debug(`No votes to enqueue for slot ${slot}`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
this.log.info(`Voting in slot ${slot} despite sync failure`, {
|
|
772
|
+
slot
|
|
773
|
+
});
|
|
774
|
+
await publisher.sendRequests();
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|
|
778
|
+
* has been there without being invalidated and whether the sequencer is in the committee or not. We always
|
|
779
|
+
* have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
|
|
780
|
+
* and if they fail, any sequencer will try as well.
|
|
781
|
+
*/ async considerInvalidatingBlock(syncedTo, currentSlot) {
|
|
782
|
+
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
783
|
+
if (pendingChainValidationStatus.valid) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const { publisher } = await this.publisherFactory.create(undefined);
|
|
787
|
+
const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
|
|
788
|
+
const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
|
|
789
|
+
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
|
|
790
|
+
const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
|
|
791
|
+
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
|
|
792
|
+
const logData = {
|
|
793
|
+
invalidL1Timestamp: invalidBlockTimestamp,
|
|
794
|
+
l1Timestamp,
|
|
795
|
+
invalidBlock: pendingChainValidationStatus.block,
|
|
796
|
+
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
797
|
+
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
798
|
+
ourValidatorAddresses,
|
|
799
|
+
currentSlot
|
|
800
|
+
};
|
|
801
|
+
const inCurrentCommittee = ()=>this.epochCache.getCommittee(currentSlot).then((c)=>c?.committee?.some((member)=>ourValidatorAddresses.some((addr)=>addr.equals(member))));
|
|
802
|
+
const invalidateAsCommitteeMember = secondsBeforeInvalidatingBlockAsCommitteeMember !== undefined && secondsBeforeInvalidatingBlockAsCommitteeMember > 0 && timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsCommitteeMember && await inCurrentCommittee();
|
|
803
|
+
const invalidateAsNonCommitteeMember = secondsBeforeInvalidatingBlockAsNonCommitteeMember !== undefined && secondsBeforeInvalidatingBlockAsNonCommitteeMember > 0 && timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsNonCommitteeMember;
|
|
804
|
+
if (!invalidateAsCommitteeMember && !invalidateAsNonCommitteeMember) {
|
|
805
|
+
this.log.debug(`Not invalidating pending chain`, logData);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
809
|
+
if (!invalidateBlock) {
|
|
810
|
+
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
|
|
814
|
+
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
815
|
+
await publisher.sendRequests();
|
|
586
816
|
}
|
|
587
|
-
|
|
588
|
-
return
|
|
817
|
+
getSlotStartBuildTimestamp(slotNumber) {
|
|
818
|
+
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
|
589
819
|
}
|
|
590
820
|
getSecondsIntoSlot(slotNumber) {
|
|
591
|
-
const slotStartTimestamp = this.
|
|
821
|
+
const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
|
|
592
822
|
return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
|
|
593
823
|
}
|
|
594
824
|
get aztecSlotDuration() {
|
|
595
825
|
return this.l1Constants.slotDuration;
|
|
596
826
|
}
|
|
597
|
-
get
|
|
598
|
-
return this.
|
|
827
|
+
get maxL2BlockGas() {
|
|
828
|
+
return this.config.maxL2BlockGas;
|
|
599
829
|
}
|
|
600
|
-
|
|
601
|
-
return this.
|
|
830
|
+
getSlasherClient() {
|
|
831
|
+
return this.slasherClient;
|
|
602
832
|
}
|
|
603
833
|
}
|
|
604
834
|
_ts_decorate([
|
|
605
835
|
trackSpan('Sequencer.work')
|
|
606
|
-
], Sequencer.prototype, "
|
|
836
|
+
], Sequencer.prototype, "safeWork", null);
|
|
607
837
|
_ts_decorate([
|
|
608
|
-
trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs,
|
|
609
|
-
[Attributes.BLOCK_NUMBER]:
|
|
838
|
+
trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
|
|
839
|
+
[Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber
|
|
610
840
|
}))
|
|
611
841
|
], Sequencer.prototype, "buildBlockAndEnqueuePublish", null);
|
|
612
842
|
_ts_decorate([
|