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