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