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