@aztec/validator-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.87a0206
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/README.md +51 -24
- package/dest/block_proposal_handler.d.ts +8 -8
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +27 -32
- package/dest/checkpoint_builder.d.ts +21 -25
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +50 -32
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -14
- package/dest/duties/validation_service.d.ts +19 -6
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +72 -19
- package/dest/factory.d.ts +2 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -1
- package/dest/key_store/ha_key_store.d.ts +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +2 -1
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +36 -6
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +10 -5
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +8 -4
- package/dest/key_store/node_keystore_adapter.d.ts +18 -5
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +18 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -5
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +8 -4
- package/dest/metrics.d.ts +4 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +34 -5
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +17 -16
- package/dest/validator.d.ts +18 -13
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +107 -81
- package/package.json +21 -17
- package/src/block_proposal_handler.ts +41 -42
- package/src/checkpoint_builder.ts +85 -38
- package/src/config.ts +7 -13
- package/src/duties/validation_service.ts +91 -23
- package/src/factory.ts +1 -0
- package/src/key_store/ha_key_store.ts +269 -0
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +44 -5
- package/src/key_store/local_key_store.ts +13 -4
- package/src/key_store/node_keystore_adapter.ts +27 -4
- package/src/key_store/web3signer_key_store.ts +17 -4
- package/src/metrics.ts +45 -6
- package/src/tx_validator/tx_validator_factory.ts +52 -31
- package/src/validator.ts +128 -94
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
2
2
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
|
+
import type { LoggerBindings } from '@aztec/foundation/log';
|
|
3
4
|
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
|
|
4
5
|
import {
|
|
5
6
|
AggregateTxValidator,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
GasTxValidator,
|
|
11
12
|
MetadataTxValidator,
|
|
12
13
|
PhasesTxValidator,
|
|
14
|
+
SizeTxValidator,
|
|
13
15
|
TimestampTxValidator,
|
|
14
16
|
TxPermittedValidator,
|
|
15
17
|
TxProofValidator,
|
|
@@ -52,31 +54,41 @@ export function createValidatorForAcceptingTxs(
|
|
|
52
54
|
blockNumber: BlockNumber;
|
|
53
55
|
txsPermitted: boolean;
|
|
54
56
|
},
|
|
57
|
+
bindings?: LoggerBindings,
|
|
55
58
|
): TxValidator<Tx> {
|
|
56
59
|
const validators: TxValidator<Tx>[] = [
|
|
57
|
-
new TxPermittedValidator(txsPermitted),
|
|
58
|
-
new
|
|
59
|
-
new
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
new
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
new TxPermittedValidator(txsPermitted, bindings),
|
|
61
|
+
new SizeTxValidator(bindings),
|
|
62
|
+
new DataTxValidator(bindings),
|
|
63
|
+
new MetadataTxValidator(
|
|
64
|
+
{
|
|
65
|
+
l1ChainId: new Fr(l1ChainId),
|
|
66
|
+
rollupVersion: new Fr(rollupVersion),
|
|
67
|
+
protocolContractsHash,
|
|
68
|
+
vkTreeRoot: getVKTreeRoot(),
|
|
69
|
+
},
|
|
70
|
+
bindings,
|
|
71
|
+
),
|
|
72
|
+
new TimestampTxValidator(
|
|
73
|
+
{
|
|
74
|
+
timestamp,
|
|
75
|
+
blockNumber,
|
|
76
|
+
},
|
|
77
|
+
bindings,
|
|
78
|
+
),
|
|
79
|
+
new DoubleSpendTxValidator(new NullifierCache(db), bindings),
|
|
80
|
+
new PhasesTxValidator(contractDataSource, setupAllowList, timestamp, bindings),
|
|
81
|
+
new BlockHeaderTxValidator(new ArchiveCache(db), bindings),
|
|
72
82
|
];
|
|
73
83
|
|
|
74
84
|
if (!skipFeeEnforcement) {
|
|
75
|
-
validators.push(
|
|
85
|
+
validators.push(
|
|
86
|
+
new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings),
|
|
87
|
+
);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
if (verifier) {
|
|
79
|
-
validators.push(new TxProofValidator(verifier));
|
|
91
|
+
validators.push(new TxProofValidator(verifier, bindings));
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
return new AggregateTxValidator(...validators);
|
|
@@ -87,6 +99,7 @@ export function createValidatorForBlockBuilding(
|
|
|
87
99
|
contractDataSource: ContractDataSource,
|
|
88
100
|
globalVariables: GlobalVariables,
|
|
89
101
|
setupAllowList: AllowedElement[],
|
|
102
|
+
bindings?: LoggerBindings,
|
|
90
103
|
): PublicProcessorValidator {
|
|
91
104
|
const nullifierCache = new NullifierCache(db);
|
|
92
105
|
const archiveCache = new ArchiveCache(db);
|
|
@@ -100,6 +113,7 @@ export function createValidatorForBlockBuilding(
|
|
|
100
113
|
contractDataSource,
|
|
101
114
|
globalVariables,
|
|
102
115
|
setupAllowList,
|
|
116
|
+
bindings,
|
|
103
117
|
),
|
|
104
118
|
nullifierCache,
|
|
105
119
|
};
|
|
@@ -112,22 +126,29 @@ function preprocessValidator(
|
|
|
112
126
|
contractDataSource: ContractDataSource,
|
|
113
127
|
globalVariables: GlobalVariables,
|
|
114
128
|
setupAllowList: AllowedElement[],
|
|
129
|
+
bindings?: LoggerBindings,
|
|
115
130
|
): TxValidator<Tx> {
|
|
116
131
|
// We don't include the TxProofValidator nor the DataTxValidator here because they are already checked by the time we get to block building.
|
|
117
132
|
return new AggregateTxValidator(
|
|
118
|
-
new MetadataTxValidator(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
new MetadataTxValidator(
|
|
134
|
+
{
|
|
135
|
+
l1ChainId: globalVariables.chainId,
|
|
136
|
+
rollupVersion: globalVariables.version,
|
|
137
|
+
protocolContractsHash,
|
|
138
|
+
vkTreeRoot: getVKTreeRoot(),
|
|
139
|
+
},
|
|
140
|
+
bindings,
|
|
141
|
+
),
|
|
142
|
+
new TimestampTxValidator(
|
|
143
|
+
{
|
|
144
|
+
timestamp: globalVariables.timestamp,
|
|
145
|
+
blockNumber: globalVariables.blockNumber,
|
|
146
|
+
},
|
|
147
|
+
bindings,
|
|
148
|
+
),
|
|
149
|
+
new DoubleSpendTxValidator(nullifierCache, bindings),
|
|
150
|
+
new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings),
|
|
151
|
+
new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, bindings),
|
|
152
|
+
new BlockHeaderTxValidator(archiveCache, bindings),
|
|
132
153
|
);
|
|
133
154
|
}
|
package/src/validator.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BlockNumber,
|
|
6
|
+
CheckpointNumber,
|
|
7
|
+
EpochNumber,
|
|
8
|
+
IndexWithinCheckpoint,
|
|
9
|
+
SlotNumber,
|
|
10
|
+
} from '@aztec/foundation/branded-types';
|
|
5
11
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
12
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
7
13
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -12,18 +18,20 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
12
18
|
import { sleep } from '@aztec/foundation/sleep';
|
|
13
19
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
14
20
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
15
|
-
import type { P2P, PeerId
|
|
21
|
+
import type { DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
16
22
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
17
23
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
18
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
19
|
-
import type { CommitteeAttestationsAndSigners,
|
|
25
|
+
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
20
27
|
import type {
|
|
21
28
|
CreateCheckpointProposalLastBlockData,
|
|
29
|
+
ITxProvider,
|
|
22
30
|
Validator,
|
|
23
31
|
ValidatorClientFullConfig,
|
|
24
32
|
WorldStateSynchronizer,
|
|
25
33
|
} from '@aztec/stdlib/interfaces/server';
|
|
26
|
-
import type
|
|
34
|
+
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
27
35
|
import type {
|
|
28
36
|
BlockProposal,
|
|
29
37
|
BlockProposalOptions,
|
|
@@ -36,6 +44,8 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
36
44
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
37
45
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
38
46
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
+
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
48
|
+
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
39
49
|
|
|
40
50
|
import { EventEmitter } from 'events';
|
|
41
51
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -43,6 +53,8 @@ import type { TypedDataDefinition } from 'viem';
|
|
|
43
53
|
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
44
54
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
45
55
|
import { ValidationService } from './duties/validation_service.js';
|
|
56
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
57
|
+
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
46
58
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
47
59
|
import { ValidatorMetrics } from './metrics.js';
|
|
48
60
|
|
|
@@ -76,13 +88,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
88
|
|
|
77
89
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
78
90
|
|
|
79
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
|
|
80
|
-
// Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
|
|
81
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
82
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
83
|
-
|
|
84
91
|
protected constructor(
|
|
85
|
-
private keyStore:
|
|
92
|
+
private keyStore: ExtendedValidatorKeyStore,
|
|
86
93
|
private epochCache: EpochCache,
|
|
87
94
|
private p2pClient: P2P,
|
|
88
95
|
private blockProposalHandler: BlockProposalHandler,
|
|
@@ -165,7 +172,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
static new(
|
|
175
|
+
static async new(
|
|
169
176
|
config: ValidatorClientFullConfig,
|
|
170
177
|
checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
171
178
|
worldState: WorldStateSynchronizer,
|
|
@@ -173,7 +180,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
173
180
|
p2pClient: P2P,
|
|
174
181
|
blockSource: L2BlockSource & L2BlockSink,
|
|
175
182
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
176
|
-
txProvider:
|
|
183
|
+
txProvider: ITxProvider,
|
|
177
184
|
keyStoreManager: KeystoreManager,
|
|
178
185
|
blobClient: BlobClientInterface,
|
|
179
186
|
dateProvider: DateProvider = new DateProvider(),
|
|
@@ -190,14 +197,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
190
197
|
l1ToL2MessageSource,
|
|
191
198
|
txProvider,
|
|
192
199
|
blockProposalValidator,
|
|
200
|
+
epochCache,
|
|
193
201
|
config,
|
|
194
202
|
metrics,
|
|
195
203
|
dateProvider,
|
|
196
204
|
telemetry,
|
|
197
205
|
);
|
|
198
206
|
|
|
207
|
+
let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
208
|
+
if (config.haSigningEnabled) {
|
|
209
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
|
+
const haConfig = {
|
|
211
|
+
...config,
|
|
212
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
|
+
};
|
|
214
|
+
const { signer } = await createHASigner(haConfig);
|
|
215
|
+
validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
|
|
216
|
+
}
|
|
217
|
+
|
|
199
218
|
const validator = new ValidatorClient(
|
|
200
|
-
|
|
219
|
+
validatorKeyStore,
|
|
201
220
|
epochCache,
|
|
202
221
|
p2pClient,
|
|
203
222
|
blockProposalHandler,
|
|
@@ -224,8 +243,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
224
243
|
return this.blockProposalHandler;
|
|
225
244
|
}
|
|
226
245
|
|
|
227
|
-
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
228
|
-
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
246
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
247
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
@@ -250,6 +269,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
250
269
|
return;
|
|
251
270
|
}
|
|
252
271
|
|
|
272
|
+
await this.keyStore.start();
|
|
273
|
+
|
|
253
274
|
await this.registerHandlers();
|
|
254
275
|
|
|
255
276
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -265,6 +286,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
265
286
|
|
|
266
287
|
public async stop() {
|
|
267
288
|
await this.epochCacheUpdateLoop.stop();
|
|
289
|
+
await this.keyStore.stop();
|
|
268
290
|
}
|
|
269
291
|
|
|
270
292
|
/** Register handlers on the p2p client */
|
|
@@ -287,6 +309,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
287
309
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
288
310
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
289
311
|
|
|
312
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
313
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
314
|
+
this.handleDuplicateProposal(info);
|
|
315
|
+
});
|
|
316
|
+
|
|
290
317
|
const myAddresses = this.getValidatorAddresses();
|
|
291
318
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
292
319
|
|
|
@@ -301,6 +328,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
301
328
|
*/
|
|
302
329
|
async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
303
330
|
const slotNumber = proposal.slotNumber;
|
|
331
|
+
|
|
332
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
333
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
334
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
335
|
+
|
|
304
336
|
const proposer = proposal.getSender();
|
|
305
337
|
|
|
306
338
|
// Reject proposals with invalid signatures
|
|
@@ -334,7 +366,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
334
366
|
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
335
367
|
proposal,
|
|
336
368
|
proposalSender,
|
|
337
|
-
!!shouldReexecute,
|
|
369
|
+
!!shouldReexecute && !escapeHatchOpen,
|
|
338
370
|
);
|
|
339
371
|
|
|
340
372
|
if (!validationResult.isValid) {
|
|
@@ -359,6 +391,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
359
391
|
|
|
360
392
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
361
393
|
if (
|
|
394
|
+
!escapeHatchOpen &&
|
|
362
395
|
validationResult.reason &&
|
|
363
396
|
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
364
397
|
slashBroadcastedInvalidBlockPenalty > 0n
|
|
@@ -373,11 +406,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
373
406
|
...proposalInfo,
|
|
374
407
|
inCommittee: partOfCommittee,
|
|
375
408
|
fishermanMode: this.config.fishermanMode || false,
|
|
409
|
+
escapeHatchOpen,
|
|
376
410
|
});
|
|
377
411
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
412
|
+
if (escapeHatchOpen) {
|
|
413
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
381
416
|
|
|
382
417
|
return true;
|
|
383
418
|
}
|
|
@@ -395,6 +430,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
395
430
|
const slotNumber = proposal.slotNumber;
|
|
396
431
|
const proposer = proposal.getSender();
|
|
397
432
|
|
|
433
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
434
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
|
|
435
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
398
439
|
// Reject proposals with invalid signatures
|
|
399
440
|
if (!proposer) {
|
|
400
441
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
@@ -417,17 +458,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
417
458
|
fishermanMode: this.config.fishermanMode || false,
|
|
418
459
|
});
|
|
419
460
|
|
|
420
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
421
|
-
// Check that we have successfully validated a block for this slot before attesting to the checkpoint.
|
|
422
|
-
if (!this.validatedBlockSlots.has(slotNumber)) {
|
|
423
|
-
this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
|
|
424
|
-
return undefined;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
461
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
462
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
463
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
431
464
|
} else {
|
|
432
465
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
433
466
|
if (!validationResult.isValid) {
|
|
@@ -490,7 +523,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
490
523
|
attestors: EthAddress[] = [],
|
|
491
524
|
): Promise<CheckpointAttestation[]> {
|
|
492
525
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
493
|
-
await this.p2pClient.
|
|
526
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
494
527
|
return attestations;
|
|
495
528
|
}
|
|
496
529
|
|
|
@@ -503,7 +536,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
536
|
proposalInfo: LogData,
|
|
504
537
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
505
538
|
const slot = proposal.slotNumber;
|
|
506
|
-
const timeoutSeconds = 10;
|
|
539
|
+
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
507
540
|
|
|
508
541
|
// Wait for last block to sync by archive
|
|
509
542
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -531,16 +564,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
564
|
return { isValid: false, reason: 'last_block_not_found' };
|
|
532
565
|
}
|
|
533
566
|
|
|
534
|
-
// Get the last full block to determine checkpoint number
|
|
535
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
536
|
-
if (!lastBlock) {
|
|
537
|
-
this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
|
|
538
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
539
|
-
}
|
|
540
|
-
const checkpointNumber = lastBlock.checkpointNumber;
|
|
541
|
-
|
|
542
567
|
// Get all full blocks for the slot and checkpoint
|
|
543
|
-
const blocks = await this.getBlocksForSlot(slot
|
|
568
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
544
569
|
if (blocks.length === 0) {
|
|
545
570
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
546
571
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
@@ -554,10 +579,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
554
579
|
// Get checkpoint constants from first block
|
|
555
580
|
const firstBlock = blocks[0];
|
|
556
581
|
const constants = this.extractCheckpointConstants(firstBlock);
|
|
582
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
557
583
|
|
|
558
584
|
// Get L1-to-L2 messages for this checkpoint
|
|
559
585
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
560
586
|
|
|
587
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
588
|
+
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
589
|
+
// actual checkpoints and the blocks/txs in them.
|
|
590
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
591
|
+
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
592
|
+
.filter(b => b.number < checkpointNumber)
|
|
593
|
+
.sort((a, b) => a.number - b.number);
|
|
594
|
+
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
595
|
+
|
|
561
596
|
// Fork world state at the block before the first block
|
|
562
597
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
563
598
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
@@ -568,8 +603,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
568
603
|
checkpointNumber,
|
|
569
604
|
constants,
|
|
570
605
|
l1ToL2Messages,
|
|
606
|
+
previousCheckpointOutHashes,
|
|
571
607
|
fork,
|
|
572
608
|
blocks,
|
|
609
|
+
this.log.getBindings(),
|
|
573
610
|
);
|
|
574
611
|
|
|
575
612
|
// Complete the checkpoint to get computed values
|
|
@@ -595,6 +632,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
595
632
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
596
633
|
}
|
|
597
634
|
|
|
635
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
636
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
637
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
638
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
639
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
640
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
641
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
642
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
643
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
644
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
645
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
646
|
+
...proposalInfo,
|
|
647
|
+
});
|
|
648
|
+
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
649
|
+
}
|
|
650
|
+
|
|
598
651
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
599
652
|
return { isValid: true };
|
|
600
653
|
} finally {
|
|
@@ -602,50 +655,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
602
655
|
}
|
|
603
656
|
}
|
|
604
657
|
|
|
605
|
-
/**
|
|
606
|
-
* Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
|
|
607
|
-
* Returns blocks in ascending order (earliest to latest).
|
|
608
|
-
* TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
|
|
609
|
-
*/
|
|
610
|
-
private async getBlocksForSlot(
|
|
611
|
-
slot: SlotNumber,
|
|
612
|
-
lastBlockHeader: BlockHeader,
|
|
613
|
-
checkpointNumber: CheckpointNumber,
|
|
614
|
-
): Promise<L2BlockNew[]> {
|
|
615
|
-
const blocks: L2BlockNew[] = [];
|
|
616
|
-
let currentHeader = lastBlockHeader;
|
|
617
|
-
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
618
|
-
|
|
619
|
-
while (currentHeader.getSlot() === slot) {
|
|
620
|
-
const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
|
|
621
|
-
if (!block) {
|
|
622
|
-
this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
if (block.checkpointNumber !== checkpointNumber) {
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
blocks.unshift(block);
|
|
629
|
-
|
|
630
|
-
const prevArchive = currentHeader.lastArchive.root;
|
|
631
|
-
if (prevArchive.equals(genesisArchiveRoot)) {
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
|
|
636
|
-
if (!prevHeader || prevHeader.getSlot() !== slot) {
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
currentHeader = prevHeader;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return blocks;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
658
|
/**
|
|
646
659
|
* Extract checkpoint global variables from a block.
|
|
647
660
|
*/
|
|
648
|
-
private extractCheckpointConstants(block:
|
|
661
|
+
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
649
662
|
const gv = block.header.globalVariables;
|
|
650
663
|
return {
|
|
651
664
|
chainId: gv.chainId,
|
|
@@ -668,14 +681,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
668
681
|
return;
|
|
669
682
|
}
|
|
670
683
|
|
|
671
|
-
|
|
672
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
673
|
-
if (!lastBlock) {
|
|
674
|
-
this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
|
|
684
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
679
685
|
if (blocks.length === 0) {
|
|
680
686
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
681
687
|
return;
|
|
@@ -720,14 +726,38 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
720
726
|
]);
|
|
721
727
|
}
|
|
722
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
731
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
732
|
+
*/
|
|
733
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
734
|
+
const { slot, proposer, type } = info;
|
|
735
|
+
|
|
736
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
737
|
+
proposer: proposer.toString(),
|
|
738
|
+
slot,
|
|
739
|
+
type,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Emit slash event
|
|
743
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
744
|
+
{
|
|
745
|
+
validator: proposer,
|
|
746
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
747
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
748
|
+
epochOrSlot: BigInt(slot),
|
|
749
|
+
},
|
|
750
|
+
]);
|
|
751
|
+
}
|
|
752
|
+
|
|
723
753
|
async createBlockProposal(
|
|
724
754
|
blockHeader: BlockHeader,
|
|
725
|
-
indexWithinCheckpoint:
|
|
755
|
+
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
726
756
|
inHash: Fr,
|
|
727
757
|
archive: Fr,
|
|
728
758
|
txs: Tx[],
|
|
729
759
|
proposerAddress: EthAddress | undefined,
|
|
730
|
-
options: BlockProposalOptions,
|
|
760
|
+
options: BlockProposalOptions = {},
|
|
731
761
|
): Promise<BlockProposal> {
|
|
732
762
|
// TODO(palla/mbps): Prevent double proposals properly
|
|
733
763
|
// if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
|
|
@@ -759,7 +789,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
759
789
|
archive: Fr,
|
|
760
790
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
761
791
|
proposerAddress: EthAddress | undefined,
|
|
762
|
-
options: CheckpointProposalOptions,
|
|
792
|
+
options: CheckpointProposalOptions = {},
|
|
763
793
|
): Promise<CheckpointProposal> {
|
|
764
794
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
765
795
|
return await this.validationService.createCheckpointProposal(
|
|
@@ -778,8 +808,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
778
808
|
async signAttestationsAndSigners(
|
|
779
809
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
780
810
|
proposer: EthAddress,
|
|
811
|
+
slot: SlotNumber,
|
|
812
|
+
blockNumber: BlockNumber | CheckpointNumber,
|
|
781
813
|
): Promise<Signature> {
|
|
782
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
814
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
|
|
783
815
|
}
|
|
784
816
|
|
|
785
817
|
async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
|
|
@@ -886,7 +918,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
886
918
|
}
|
|
887
919
|
|
|
888
920
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
889
|
-
|
|
921
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
922
|
+
const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
|
|
923
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
890
924
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
891
925
|
return authResponse.toBuffer();
|
|
892
926
|
}
|