@aztec/validator-client 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/block_proposal_handler.d.ts +53 -0
- package/dest/block_proposal_handler.d.ts.map +1 -0
- package/dest/block_proposal_handler.js +290 -0
- package/dest/config.d.ts +4 -20
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +18 -2
- package/dest/duties/validation_service.d.ts +11 -6
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +19 -7
- package/dest/factory.d.ts +14 -5
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +10 -0
- package/dest/index.d.ts +2 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -0
- package/dest/key_store/index.d.ts +1 -1
- package/dest/key_store/interface.d.ts +1 -1
- package/dest/key_store/local_key_store.d.ts +1 -1
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +1 -1
- package/dest/key_store/node_keystore_adapter.d.ts +1 -1
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +2 -4
- package/dest/key_store/web3signer_key_store.d.ts +1 -7
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +7 -8
- package/dest/metrics.d.ts +7 -5
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +25 -12
- package/dest/validator.d.ts +28 -38
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +182 -205
- package/package.json +15 -15
- package/src/block_proposal_handler.ts +346 -0
- package/src/config.ts +30 -25
- package/src/duties/validation_service.ts +30 -12
- package/src/factory.ts +34 -4
- package/src/index.ts +1 -0
- package/src/key_store/local_key_store.ts +1 -1
- package/src/key_store/node_keystore_adapter.ts +3 -4
- package/src/key_store/web3signer_key_store.ts +7 -10
- package/src/metrics.ts +34 -13
- package/src/validator.ts +252 -299
package/src/validator.ts
CHANGED
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
4
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
-
import {
|
|
5
|
-
import { createLogger } from '@aztec/foundation/log';
|
|
6
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
5
|
+
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
6
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
7
7
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
8
8
|
import { sleep } from '@aztec/foundation/sleep';
|
|
9
|
-
import { DateProvider
|
|
9
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
10
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
11
|
-
import type { P2P, PeerId } from '@aztec/p2p';
|
|
12
|
-
import { AuthRequest, AuthResponse,
|
|
13
|
-
import {
|
|
14
|
-
import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
|
|
15
|
-
import {
|
|
16
|
-
OffenseType,
|
|
17
|
-
type SlasherConfig,
|
|
18
|
-
WANT_TO_SLASH_EVENT,
|
|
19
|
-
type Watcher,
|
|
20
|
-
type WatcherEmitter,
|
|
21
|
-
} from '@aztec/slasher';
|
|
11
|
+
import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
|
|
12
|
+
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
13
|
+
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
22
14
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
23
|
-
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
24
|
-
import {
|
|
25
|
-
import type { IFullNodeBlockBuilder, SequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
15
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSource } from '@aztec/stdlib/block';
|
|
16
|
+
import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server';
|
|
26
17
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
27
18
|
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
ReExFailedTxsError,
|
|
32
|
-
ReExStateMismatchError,
|
|
33
|
-
ReExTimeoutError,
|
|
34
|
-
TransactionsNotAvailableError,
|
|
35
|
-
} from '@aztec/stdlib/validators';
|
|
19
|
+
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
20
|
+
import type { Tx } from '@aztec/stdlib/tx';
|
|
21
|
+
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
36
22
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
37
23
|
|
|
38
24
|
import { EventEmitter } from 'events';
|
|
39
25
|
import type { TypedDataDefinition } from 'viem';
|
|
40
26
|
|
|
41
|
-
import type
|
|
27
|
+
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
42
28
|
import { ValidationService } from './duties/validation_service.js';
|
|
43
29
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
44
30
|
import { ValidatorMetrics } from './metrics.js';
|
|
@@ -47,25 +33,11 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
47
33
|
// Just cap the set to avoid unbounded growth.
|
|
48
34
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
createBlockProposal(
|
|
56
|
-
blockNumber: number,
|
|
57
|
-
header: ProposedBlockHeader,
|
|
58
|
-
archive: Fr,
|
|
59
|
-
stateReference: StateReference,
|
|
60
|
-
txs: Tx[],
|
|
61
|
-
proposerAddress: EthAddress | undefined,
|
|
62
|
-
options: BlockProposalOptions,
|
|
63
|
-
): Promise<BlockProposal | undefined>;
|
|
64
|
-
attestToProposal(proposal: BlockProposal, sender: PeerId): Promise<BlockAttestation[] | undefined>;
|
|
65
|
-
|
|
66
|
-
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
67
|
-
collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
|
|
68
|
-
}
|
|
36
|
+
// What errors from the block proposal handler result in slashing
|
|
37
|
+
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
38
|
+
'state_mismatch',
|
|
39
|
+
'failed_txs',
|
|
40
|
+
];
|
|
69
41
|
|
|
70
42
|
/**
|
|
71
43
|
* Validator Client
|
|
@@ -74,57 +46,57 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
74
46
|
public readonly tracer: Tracer;
|
|
75
47
|
private validationService: ValidationService;
|
|
76
48
|
private metrics: ValidatorMetrics;
|
|
49
|
+
private log: Logger;
|
|
50
|
+
|
|
51
|
+
// Whether it has already registered handlers on the p2p client
|
|
52
|
+
private hasRegisteredHandlers = false;
|
|
77
53
|
|
|
78
54
|
// Used to check if we are sending the same proposal twice
|
|
79
55
|
private previousProposal?: BlockProposal;
|
|
80
56
|
|
|
81
|
-
private
|
|
82
|
-
private lastEpochForCommitteeUpdateLoop: bigint | undefined;
|
|
57
|
+
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
83
58
|
private epochCacheUpdateLoop: RunningPromise;
|
|
84
59
|
|
|
85
|
-
private blockProposalValidator: BlockProposalValidator;
|
|
86
|
-
|
|
87
60
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
88
61
|
|
|
89
62
|
protected constructor(
|
|
90
|
-
private blockBuilder: IFullNodeBlockBuilder,
|
|
91
63
|
private keyStore: NodeKeystoreAdapter,
|
|
92
64
|
private epochCache: EpochCache,
|
|
93
65
|
private p2pClient: P2P,
|
|
94
|
-
private
|
|
95
|
-
private
|
|
96
|
-
private txProvider: TxProvider,
|
|
97
|
-
private config: ValidatorClientConfig &
|
|
98
|
-
Pick<SequencerConfig, 'txPublicSetupAllowList'> &
|
|
99
|
-
Pick<SlasherConfig, 'slashBroadcastedInvalidBlockPenalty'>,
|
|
66
|
+
private blockProposalHandler: BlockProposalHandler,
|
|
67
|
+
private config: ValidatorClientFullConfig,
|
|
100
68
|
private dateProvider: DateProvider = new DateProvider(),
|
|
101
69
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
102
|
-
|
|
70
|
+
log = createLogger('validator'),
|
|
103
71
|
) {
|
|
104
72
|
super();
|
|
73
|
+
|
|
74
|
+
// Create child logger with fisherman prefix if in fisherman mode
|
|
75
|
+
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
76
|
+
|
|
105
77
|
this.tracer = telemetry.getTracer('Validator');
|
|
106
78
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
107
79
|
|
|
108
|
-
this.validationService = new ValidationService(keyStore);
|
|
109
|
-
|
|
110
|
-
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
80
|
+
this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
|
|
111
81
|
|
|
112
82
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
113
|
-
this.
|
|
114
|
-
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
83
|
+
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
115
84
|
|
|
116
|
-
|
|
85
|
+
const myAddresses = this.getValidatorAddresses();
|
|
86
|
+
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
117
87
|
}
|
|
118
88
|
|
|
119
|
-
public static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager) {
|
|
89
|
+
public static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager, logger?: Logger) {
|
|
120
90
|
const validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
121
91
|
const validatorAddresses = validatorKeyStore.getAddresses();
|
|
122
92
|
// Verify that we can retrieve all required data from the key store
|
|
123
93
|
for (const address of validatorAddresses) {
|
|
124
94
|
// Functions throw if required data is not available
|
|
95
|
+
let coinbase: EthAddress;
|
|
96
|
+
let feeRecipient: AztecAddress;
|
|
125
97
|
try {
|
|
126
|
-
validatorKeyStore.getCoinbaseAddress(address);
|
|
127
|
-
validatorKeyStore.getFeeRecipient(address);
|
|
98
|
+
coinbase = validatorKeyStore.getCoinbaseAddress(address);
|
|
99
|
+
feeRecipient = validatorKeyStore.getFeeRecipient(address);
|
|
128
100
|
} catch (error) {
|
|
129
101
|
throw new Error(`Failed to retrieve required data for validator address ${address}, error: ${error}`);
|
|
130
102
|
}
|
|
@@ -133,6 +105,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
133
105
|
if (!publisherAddresses.length) {
|
|
134
106
|
throw new Error(`No publisher addresses found for validator address ${address}`);
|
|
135
107
|
}
|
|
108
|
+
logger?.debug(
|
|
109
|
+
`Validator ${address.toString()} configured with coinbase ${coinbase.toString()}, feeRecipient ${feeRecipient.toString()} and publishers ${publisherAddresses.map(x => x.toString()).join()}`,
|
|
110
|
+
);
|
|
136
111
|
}
|
|
137
112
|
}
|
|
138
113
|
|
|
@@ -144,7 +119,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
144
119
|
return;
|
|
145
120
|
}
|
|
146
121
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
147
|
-
const me = this.
|
|
122
|
+
const me = this.getValidatorAddresses();
|
|
148
123
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
149
124
|
const inCommittee = me.filter(a => committeeSet.has(a.toString()));
|
|
150
125
|
if (inCommittee.length > 0) {
|
|
@@ -164,7 +139,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
164
139
|
}
|
|
165
140
|
|
|
166
141
|
static new(
|
|
167
|
-
config:
|
|
142
|
+
config: ValidatorClientFullConfig,
|
|
168
143
|
blockBuilder: IFullNodeBlockBuilder,
|
|
169
144
|
epochCache: EpochCache,
|
|
170
145
|
p2pClient: P2P,
|
|
@@ -175,26 +150,53 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
175
150
|
dateProvider: DateProvider = new DateProvider(),
|
|
176
151
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
177
152
|
) {
|
|
178
|
-
const
|
|
153
|
+
const metrics = new ValidatorMetrics(telemetry);
|
|
154
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
155
|
+
txsPermitted: !config.disableTransactions,
|
|
156
|
+
});
|
|
157
|
+
const blockProposalHandler = new BlockProposalHandler(
|
|
179
158
|
blockBuilder,
|
|
180
|
-
NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
|
|
181
|
-
epochCache,
|
|
182
|
-
p2pClient,
|
|
183
159
|
blockSource,
|
|
184
160
|
l1ToL2MessageSource,
|
|
185
161
|
txProvider,
|
|
162
|
+
blockProposalValidator,
|
|
163
|
+
config,
|
|
164
|
+
metrics,
|
|
165
|
+
dateProvider,
|
|
166
|
+
telemetry,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const validator = new ValidatorClient(
|
|
170
|
+
NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
|
|
171
|
+
epochCache,
|
|
172
|
+
p2pClient,
|
|
173
|
+
blockProposalHandler,
|
|
186
174
|
config,
|
|
187
175
|
dateProvider,
|
|
188
176
|
telemetry,
|
|
189
177
|
);
|
|
190
178
|
|
|
191
|
-
// TODO(PhilWindle): This seems like it could/should be done inside start()
|
|
192
|
-
validator.registerBlockProposalHandler();
|
|
193
179
|
return validator;
|
|
194
180
|
}
|
|
195
181
|
|
|
196
182
|
public getValidatorAddresses() {
|
|
197
|
-
return this.keyStore
|
|
183
|
+
return this.keyStore
|
|
184
|
+
.getAddresses()
|
|
185
|
+
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public getBlockProposalHandler() {
|
|
189
|
+
return this.blockProposalHandler;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Proxy method for backwards compatibility with tests
|
|
193
|
+
public reExecuteTransactions(
|
|
194
|
+
proposal: BlockProposal,
|
|
195
|
+
blockNumber: BlockNumber,
|
|
196
|
+
txs: any[],
|
|
197
|
+
l1ToL2Messages: Fr[],
|
|
198
|
+
): Promise<any> {
|
|
199
|
+
return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages);
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
@@ -209,32 +211,30 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
209
211
|
return this.keyStore.getFeeRecipient(attestor);
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
public
|
|
213
|
-
this.config
|
|
214
|
-
|
|
214
|
+
public getConfig(): ValidatorClientFullConfig {
|
|
215
|
+
return this.config;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
219
|
+
this.config = { ...this.config, ...config };
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
public async start() {
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
224
|
+
this.log.warn(`Validator client already started`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
220
227
|
|
|
221
|
-
|
|
228
|
+
await this.registerHandlers();
|
|
222
229
|
|
|
230
|
+
const myAddresses = this.getValidatorAddresses();
|
|
223
231
|
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
232
|
+
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
224
233
|
if (inCommittee.length > 0) {
|
|
225
|
-
this.log.info(
|
|
226
|
-
`Started validator with addresses in current validator committee: ${inCommittee
|
|
227
|
-
.map(a => a.toString())
|
|
228
|
-
.join(', ')}`,
|
|
229
|
-
);
|
|
230
|
-
} else {
|
|
231
|
-
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
234
|
+
this.log.info(`Addresses in current validator committee: ${inCommittee.map(a => a.toString()).join(', ')}`);
|
|
232
235
|
}
|
|
233
236
|
this.epochCacheUpdateLoop.start();
|
|
234
237
|
|
|
235
|
-
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
236
|
-
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
237
|
-
|
|
238
238
|
return Promise.resolve();
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -242,223 +242,146 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
242
242
|
await this.epochCacheUpdateLoop.stop();
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
/** Register handlers on the p2p client */
|
|
246
|
+
public async registerHandlers() {
|
|
247
|
+
if (!this.hasRegisteredHandlers) {
|
|
248
|
+
this.hasRegisteredHandlers = true;
|
|
249
|
+
this.log.debug(`Registering validator handlers for p2p client`);
|
|
250
|
+
|
|
251
|
+
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
|
|
252
|
+
this.attestToProposal(block, proposalSender);
|
|
253
|
+
this.p2pClient.registerBlockProposalHandler(handler);
|
|
254
|
+
|
|
255
|
+
const myAddresses = this.getValidatorAddresses();
|
|
256
|
+
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
257
|
+
|
|
258
|
+
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
259
|
+
}
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
|
|
252
|
-
const slotNumber = proposal.slotNumber
|
|
253
|
-
const blockNumber = proposal.blockNumber;
|
|
263
|
+
const slotNumber = proposal.slotNumber;
|
|
254
264
|
const proposer = proposal.getSender();
|
|
255
265
|
|
|
266
|
+
// Reject proposals with invalid signatures
|
|
267
|
+
if (!proposer) {
|
|
268
|
+
this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
256
272
|
// Check that I have any address in current committee before attesting
|
|
257
|
-
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.
|
|
273
|
+
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
258
274
|
const partOfCommittee = inCommittee.length > 0;
|
|
259
275
|
|
|
260
|
-
const proposalInfo = {
|
|
261
|
-
...proposal.toBlockInfo(),
|
|
262
|
-
proposer: proposer.toString(),
|
|
263
|
-
};
|
|
264
|
-
|
|
276
|
+
const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() };
|
|
265
277
|
this.log.info(`Received proposal for slot ${slotNumber}`, {
|
|
266
278
|
...proposalInfo,
|
|
267
|
-
txHashes: proposal.txHashes.map(
|
|
279
|
+
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
280
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
268
281
|
});
|
|
269
282
|
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (invalidProposal) {
|
|
287
|
-
this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
|
|
288
|
-
if (partOfCommittee) {
|
|
289
|
-
this.metrics.incFailedAttestations(1, 'invalid_proposal');
|
|
290
|
-
}
|
|
291
|
-
return undefined;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
295
|
-
// Q: Should we move this to the block proposal validator? If there, then p2p would check it
|
|
296
|
-
// before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
|
|
297
|
-
// would not be rebroadcasted. But it also means that nodes that have not fully synced would
|
|
298
|
-
// not rebroadcast the proposal.
|
|
299
|
-
if (blockNumber > INITIAL_L2_BLOCK_NUM) {
|
|
300
|
-
const config = this.blockBuilder.getConfig();
|
|
301
|
-
const deadline = this.getReexecutionDeadline(proposal, config);
|
|
302
|
-
const currentTime = this.dateProvider.now();
|
|
303
|
-
const timeoutDurationMs = deadline.getTime() - currentTime;
|
|
304
|
-
const parentBlock =
|
|
305
|
-
timeoutDurationMs <= 0
|
|
306
|
-
? undefined
|
|
307
|
-
: await retryUntil(
|
|
308
|
-
async () => {
|
|
309
|
-
const block = await this.blockSource.getBlock(blockNumber - 1);
|
|
310
|
-
if (block) {
|
|
311
|
-
return block;
|
|
312
|
-
}
|
|
313
|
-
await this.blockSource.syncImmediate();
|
|
314
|
-
return await this.blockSource.getBlock(blockNumber - 1);
|
|
315
|
-
},
|
|
316
|
-
'Force Archiver Sync',
|
|
317
|
-
timeoutDurationMs / 1000, // Continue retrying until the deadline
|
|
318
|
-
0.5, // Retry every 500ms
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
if (parentBlock === undefined) {
|
|
322
|
-
this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
|
|
323
|
-
if (partOfCommittee) {
|
|
324
|
-
this.metrics.incFailedAttestations(1, 'parent_block_not_found');
|
|
325
|
-
}
|
|
326
|
-
return undefined;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
330
|
-
this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
|
|
331
|
-
proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
|
|
332
|
-
parentBlockArchiveRoot: parentBlock.archive.root.toString(),
|
|
333
|
-
...proposalInfo,
|
|
334
|
-
});
|
|
335
|
-
if (partOfCommittee) {
|
|
336
|
-
this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
|
|
337
|
-
}
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
283
|
+
// Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
|
|
284
|
+
// invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
|
|
285
|
+
// In fisherman mode, we always reexecute to validate proposals.
|
|
286
|
+
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
|
|
287
|
+
this.config;
|
|
288
|
+
const shouldReexecute =
|
|
289
|
+
fishermanMode ||
|
|
290
|
+
(slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
|
|
291
|
+
(partOfCommittee && validatorReexecute) ||
|
|
292
|
+
alwaysReexecuteBlockProposals;
|
|
293
|
+
|
|
294
|
+
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
295
|
+
proposal,
|
|
296
|
+
proposalSender,
|
|
297
|
+
!!shouldReexecute,
|
|
298
|
+
);
|
|
341
299
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
300
|
+
if (!validationResult.isValid) {
|
|
301
|
+
this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
302
|
+
|
|
303
|
+
const reason = validationResult.reason || 'unknown';
|
|
304
|
+
// Classify failure reason: bad proposal vs node issue
|
|
305
|
+
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
306
|
+
'invalid_proposal',
|
|
307
|
+
'state_mismatch',
|
|
308
|
+
'failed_txs',
|
|
309
|
+
'in_hash_mismatch',
|
|
310
|
+
'parent_block_wrong_slot',
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
|
|
314
|
+
this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
|
|
315
|
+
} else {
|
|
316
|
+
// Node issues so we can't attest
|
|
317
|
+
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
355
318
|
}
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
358
319
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return undefined;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Try re-executing the transactions in the proposal
|
|
369
|
-
try {
|
|
370
|
-
this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
|
|
371
|
-
if (this.config.validatorReexecute) {
|
|
372
|
-
this.log.verbose(`Re-executing transactions in the proposal before attesting`);
|
|
373
|
-
await this.reExecuteTransactions(proposal, txs, l1ToL2Messages);
|
|
374
|
-
}
|
|
375
|
-
} catch (error: any) {
|
|
376
|
-
this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
|
|
377
|
-
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
378
|
-
if (error instanceof ReExStateMismatchError && this.config.slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
320
|
+
// Slash invalid block proposals (can happen even when not in committee)
|
|
321
|
+
if (
|
|
322
|
+
validationResult.reason &&
|
|
323
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
324
|
+
slashBroadcastedInvalidBlockPenalty > 0n
|
|
325
|
+
) {
|
|
379
326
|
this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
|
|
380
327
|
this.slashInvalidBlock(proposal);
|
|
381
328
|
}
|
|
382
329
|
return undefined;
|
|
383
330
|
}
|
|
384
331
|
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
this.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return this.doAttestToProposal(proposal, inCommittee);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
private getReexecutionDeadline(
|
|
394
|
-
proposal: BlockProposal,
|
|
395
|
-
config: { l1GenesisTime: bigint; slotDuration: number },
|
|
396
|
-
): Date {
|
|
397
|
-
const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
|
|
398
|
-
const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
|
|
399
|
-
return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Re-execute the transactions in the proposal and check that the state updates match the header state
|
|
404
|
-
* @param proposal - The proposal to re-execute
|
|
405
|
-
*/
|
|
406
|
-
async reExecuteTransactions(proposal: BlockProposal, txs: Tx[], l1ToL2Messages: Fr[]): Promise<void> {
|
|
407
|
-
const { header } = proposal.payload;
|
|
408
|
-
const { txHashes } = proposal;
|
|
409
|
-
|
|
410
|
-
// If we do not have all of the transactions, then we should fail
|
|
411
|
-
if (txs.length !== txHashes.length) {
|
|
412
|
-
const foundTxHashes = txs.map(tx => tx.getTxHash());
|
|
413
|
-
const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
|
|
414
|
-
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
332
|
+
// Check that I have any address in current committee before attesting
|
|
333
|
+
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
334
|
+
if (!partOfCommittee && !this.config.fishermanMode) {
|
|
335
|
+
this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
|
|
336
|
+
return undefined;
|
|
415
337
|
}
|
|
416
338
|
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
blockNumber: proposal.blockNumber,
|
|
423
|
-
timestamp: header.timestamp,
|
|
424
|
-
chainId: new Fr(config.l1ChainId),
|
|
425
|
-
version: new Fr(config.rollupVersion),
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, globalVariables, {
|
|
429
|
-
deadline: this.getReexecutionDeadline(proposal, config),
|
|
339
|
+
// Provided all of the above checks pass, we can attest to the proposal
|
|
340
|
+
this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} proposal for slot ${slotNumber}`, {
|
|
341
|
+
...proposalInfo,
|
|
342
|
+
inCommittee: partOfCommittee,
|
|
343
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
430
344
|
});
|
|
431
345
|
|
|
432
|
-
this.
|
|
433
|
-
const numFailedTxs = failedTxs.length;
|
|
346
|
+
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
434
347
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
348
|
+
// If the above function does not throw an error, then we can attest to the proposal
|
|
349
|
+
// Determine which validators should attest
|
|
350
|
+
let attestors: EthAddress[];
|
|
351
|
+
if (partOfCommittee) {
|
|
352
|
+
attestors = inCommittee;
|
|
353
|
+
} else if (this.config.fishermanMode) {
|
|
354
|
+
// In fisherman mode, create attestations for validation purposes even if not in committee. These won't be broadcast.
|
|
355
|
+
attestors = this.getValidatorAddresses();
|
|
356
|
+
} else {
|
|
357
|
+
attestors = [];
|
|
438
358
|
}
|
|
439
359
|
|
|
440
|
-
if
|
|
441
|
-
|
|
442
|
-
|
|
360
|
+
// Only create attestations if we have attestors
|
|
361
|
+
if (attestors.length === 0) {
|
|
362
|
+
return undefined;
|
|
443
363
|
}
|
|
444
364
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
this.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
block.header.state,
|
|
453
|
-
);
|
|
365
|
+
if (this.config.fishermanMode) {
|
|
366
|
+
// bail out early and don't save attestations to the pool in fisherman mode
|
|
367
|
+
this.log.info(`Creating attestations for proposal for slot ${slotNumber}`, {
|
|
368
|
+
...proposalInfo,
|
|
369
|
+
attestors: attestors.map(a => a.toString()),
|
|
370
|
+
});
|
|
371
|
+
return undefined;
|
|
454
372
|
}
|
|
455
|
-
|
|
456
|
-
this.metrics.recordReex(timer.ms(), txs.length, block.header.totalManaUsed.toNumber() / 1e6);
|
|
373
|
+
return this.createBlockAttestationsFromProposal(proposal, attestors);
|
|
457
374
|
}
|
|
458
375
|
|
|
459
376
|
private slashInvalidBlock(proposal: BlockProposal) {
|
|
460
377
|
const proposer = proposal.getSender();
|
|
461
378
|
|
|
379
|
+
// Skip if signature is invalid (shouldn't happen since we validate earlier)
|
|
380
|
+
if (!proposer) {
|
|
381
|
+
this.log.warn(`Cannot slash proposal with invalid signature`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
462
385
|
// Trim the set if it's too big.
|
|
463
386
|
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
464
387
|
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
@@ -472,34 +395,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
472
395
|
validator: proposer,
|
|
473
396
|
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
474
397
|
offenseType: OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL,
|
|
475
|
-
epochOrSlot: proposal.slotNumber
|
|
398
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
476
399
|
},
|
|
477
400
|
]);
|
|
478
401
|
}
|
|
479
402
|
|
|
480
403
|
async createBlockProposal(
|
|
481
|
-
blockNumber:
|
|
482
|
-
header:
|
|
404
|
+
blockNumber: BlockNumber,
|
|
405
|
+
header: CheckpointHeader,
|
|
483
406
|
archive: Fr,
|
|
484
|
-
stateReference: StateReference,
|
|
485
407
|
txs: Tx[],
|
|
486
408
|
proposerAddress: EthAddress | undefined,
|
|
487
409
|
options: BlockProposalOptions,
|
|
488
410
|
): Promise<BlockProposal | undefined> {
|
|
489
|
-
if (this.previousProposal?.slotNumber
|
|
411
|
+
if (this.previousProposal?.slotNumber === header.slotNumber) {
|
|
490
412
|
this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
491
413
|
return Promise.resolve(undefined);
|
|
492
414
|
}
|
|
493
415
|
|
|
494
|
-
const newProposal = await this.validationService.createBlockProposal(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
stateReference,
|
|
499
|
-
txs,
|
|
500
|
-
proposerAddress,
|
|
501
|
-
options,
|
|
502
|
-
);
|
|
416
|
+
const newProposal = await this.validationService.createBlockProposal(header, archive, txs, proposerAddress, {
|
|
417
|
+
...options,
|
|
418
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
419
|
+
});
|
|
503
420
|
this.previousProposal = newProposal;
|
|
504
421
|
return newProposal;
|
|
505
422
|
}
|
|
@@ -508,16 +425,31 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
508
425
|
await this.p2pClient.broadcastProposal(proposal);
|
|
509
426
|
}
|
|
510
427
|
|
|
428
|
+
async signAttestationsAndSigners(
|
|
429
|
+
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
430
|
+
proposer: EthAddress,
|
|
431
|
+
): Promise<Signature> {
|
|
432
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
433
|
+
}
|
|
434
|
+
|
|
511
435
|
async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
|
|
512
|
-
const slot = proposal.payload.header.slotNumber
|
|
513
|
-
const inCommittee = await this.epochCache.filterInCommittee(slot, this.
|
|
436
|
+
const slot = proposal.payload.header.slotNumber;
|
|
437
|
+
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
514
438
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
515
|
-
|
|
439
|
+
const attestations = await this.createBlockAttestationsFromProposal(proposal, inCommittee);
|
|
440
|
+
|
|
441
|
+
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
442
|
+
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
443
|
+
// due to inactivity for missed attestations.
|
|
444
|
+
void this.p2pClient.broadcastAttestations(attestations).catch(err => {
|
|
445
|
+
this.log.error(`Failed to broadcast self-attestations for slot ${slot}`, err);
|
|
446
|
+
});
|
|
447
|
+
return attestations;
|
|
516
448
|
}
|
|
517
449
|
|
|
518
450
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
519
451
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
520
|
-
const slot = proposal.payload.header.slotNumber
|
|
452
|
+
const slot = proposal.payload.header.slotNumber;
|
|
521
453
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
522
454
|
|
|
523
455
|
if (+deadline < this.dateProvider.now()) {
|
|
@@ -530,17 +462,37 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
530
462
|
await this.collectOwnAttestations(proposal);
|
|
531
463
|
|
|
532
464
|
const proposalId = proposal.archive.toString();
|
|
533
|
-
const myAddresses = this.
|
|
465
|
+
const myAddresses = this.getValidatorAddresses();
|
|
534
466
|
|
|
535
467
|
let attestations: BlockAttestation[] = [];
|
|
536
468
|
while (true) {
|
|
537
|
-
|
|
469
|
+
// Filter out attestations with a mismatching payload. This should NOT happen since we have verified
|
|
470
|
+
// the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
|
|
471
|
+
const collectedAttestations = (await this.p2pClient.getAttestationsForSlot(slot, proposalId)).filter(
|
|
472
|
+
attestation => {
|
|
473
|
+
if (!attestation.payload.equals(proposal.payload)) {
|
|
474
|
+
this.log.warn(
|
|
475
|
+
`Received attestation for slot ${slot} with mismatched payload from ${attestation.getSender()?.toString()}`,
|
|
476
|
+
{ attestationPayload: attestation.payload, proposalPayload: proposal.payload },
|
|
477
|
+
);
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Log new attestations we collected
|
|
538
485
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
539
486
|
for (const collected of collectedAttestations) {
|
|
540
487
|
const collectedSender = collected.getSender();
|
|
488
|
+
// Skip attestations with invalid signatures
|
|
489
|
+
if (!collectedSender) {
|
|
490
|
+
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
541
493
|
if (
|
|
542
494
|
!myAddresses.some(address => address.equals(collectedSender)) &&
|
|
543
|
-
!oldSenders.some(sender => sender
|
|
495
|
+
!oldSenders.some(sender => sender?.equals(collectedSender))
|
|
544
496
|
) {
|
|
545
497
|
this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
|
|
546
498
|
}
|
|
@@ -557,12 +509,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
557
509
|
throw new AttestationTimeoutError(attestations.length, required, slot);
|
|
558
510
|
}
|
|
559
511
|
|
|
560
|
-
this.log.debug(`Collected ${attestations.length} attestations so far`);
|
|
512
|
+
this.log.debug(`Collected ${attestations.length} of ${required} attestations so far`);
|
|
561
513
|
await sleep(this.config.attestationPollingIntervalMs);
|
|
562
514
|
}
|
|
563
515
|
}
|
|
564
516
|
|
|
565
|
-
private async
|
|
517
|
+
private async createBlockAttestationsFromProposal(
|
|
518
|
+
proposal: BlockProposal,
|
|
519
|
+
attestors: EthAddress[] = [],
|
|
520
|
+
): Promise<BlockAttestation[]> {
|
|
566
521
|
const attestations = await this.validationService.attestToProposal(proposal, attestors);
|
|
567
522
|
await this.p2pClient.addAttestations(attestations);
|
|
568
523
|
return attestations;
|
|
@@ -591,5 +546,3 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
591
546
|
return authResponse.toBuffer();
|
|
592
547
|
}
|
|
593
548
|
}
|
|
594
|
-
|
|
595
|
-
// Conversion helpers moved into NodeKeystoreAdapter.
|