@aztec/validator-client 0.0.1-commit.1142ef1 → 0.0.1-commit.1bea0213
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 +41 -15
- package/dest/block_proposal_handler.d.ts +7 -6
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +23 -29
- package/dest/checkpoint_builder.d.ts +18 -21
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +17 -12
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +6 -11
- 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/tx_validator/tx_validator_factory.d.ts +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +2 -1
- package/dest/validator.d.ts +9 -8
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +68 -60
- package/package.json +19 -17
- package/src/block_proposal_handler.ts +34 -36
- package/src/checkpoint_builder.ts +37 -20
- package/src/config.ts +5 -10
- 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/tx_validator/tx_validator_factory.ts +2 -0
- package/src/validator.ts +85 -69
|
@@ -6,6 +6,7 @@ import { KeystoreManager, loadKeystoreFile } from '@aztec/node-keystore';
|
|
|
6
6
|
import type { EthRemoteSignerConfig } from '@aztec/node-keystore';
|
|
7
7
|
import { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
8
8
|
import { InvalidValidatorPrivateKeyError } from '@aztec/stdlib/validators';
|
|
9
|
+
import type { SigningContext } from '@aztec/validator-ha-signer/types';
|
|
9
10
|
|
|
10
11
|
import type { TypedDataDefinition } from 'viem';
|
|
11
12
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
@@ -230,9 +231,10 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
|
|
|
230
231
|
/**
|
|
231
232
|
* Sign typed data with all attester signers across validators.
|
|
232
233
|
* @param typedData EIP-712 typed data
|
|
234
|
+
* @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
|
|
233
235
|
* @returns Array of signatures in validator order, flattened
|
|
234
236
|
*/
|
|
235
|
-
async signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
|
|
237
|
+
async signTypedData(typedData: TypedDataDefinition, _context: SigningContext): Promise<Signature[]> {
|
|
236
238
|
const jobs: Promise<Signature>[] = [];
|
|
237
239
|
for (const i of this.validatorIndices()) {
|
|
238
240
|
const v = this.ensureValidator(i);
|
|
@@ -246,9 +248,10 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
|
|
|
246
248
|
/**
|
|
247
249
|
* Sign a message with all attester signers across validators.
|
|
248
250
|
* @param message 32-byte message (already hashed/padded as needed)
|
|
251
|
+
* @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
|
|
249
252
|
* @returns Array of signatures in validator order, flattened
|
|
250
253
|
*/
|
|
251
|
-
async signMessage(message: Buffer32): Promise<Signature[]> {
|
|
254
|
+
async signMessage(message: Buffer32, _context: SigningContext): Promise<Signature[]> {
|
|
252
255
|
const jobs: Promise<Signature>[] = [];
|
|
253
256
|
for (const i of this.validatorIndices()) {
|
|
254
257
|
const v = this.ensureValidator(i);
|
|
@@ -264,10 +267,15 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
|
|
|
264
267
|
* Hydrates caches on-demand when the address is first seen.
|
|
265
268
|
* @param address Address to sign with
|
|
266
269
|
* @param typedData EIP-712 typed data
|
|
270
|
+
* @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
|
|
267
271
|
* @returns Signature from the signer matching the address
|
|
268
272
|
* @throws Error when no signer exists for the address
|
|
269
273
|
*/
|
|
270
|
-
async signTypedDataWithAddress(
|
|
274
|
+
async signTypedDataWithAddress(
|
|
275
|
+
address: EthAddress,
|
|
276
|
+
typedData: TypedDataDefinition,
|
|
277
|
+
_context: SigningContext,
|
|
278
|
+
): Promise<Signature> {
|
|
271
279
|
const entry = this.addressIndex.get(NodeKeystoreAdapter.key(address));
|
|
272
280
|
if (entry) {
|
|
273
281
|
return await this.keystoreManager.signTypedData(entry.signer, typedData);
|
|
@@ -290,10 +298,11 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
|
|
|
290
298
|
* Hydrates caches on-demand when the address is first seen.
|
|
291
299
|
* @param address Address to sign with
|
|
292
300
|
* @param message 32-byte message
|
|
301
|
+
* @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
|
|
293
302
|
* @returns Signature from the signer matching the address
|
|
294
303
|
* @throws Error when no signer exists for the address
|
|
295
304
|
*/
|
|
296
|
-
async signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
|
|
305
|
+
async signMessageWithAddress(address: EthAddress, message: Buffer32, _context: SigningContext): Promise<Signature> {
|
|
297
306
|
const entry = this.addressIndex.get(NodeKeystoreAdapter.key(address));
|
|
298
307
|
if (entry) {
|
|
299
308
|
return await this.keystoreManager.signMessage(entry.signer, message);
|
|
@@ -372,4 +381,18 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
|
|
|
372
381
|
const validatorIndex = this.findValidatorIndexForAttester(attesterAddress);
|
|
373
382
|
return this.keystoreManager.getEffectiveRemoteSignerConfig(validatorIndex, attesterAddress);
|
|
374
383
|
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Start the key store - no-op
|
|
387
|
+
*/
|
|
388
|
+
start(): Promise<void> {
|
|
389
|
+
return Promise.resolve();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Stop the key store - no-op
|
|
394
|
+
*/
|
|
395
|
+
stop(): Promise<void> {
|
|
396
|
+
return Promise.resolve();
|
|
397
|
+
}
|
|
375
398
|
}
|
|
@@ -2,6 +2,7 @@ import type { Buffer32 } from '@aztec/foundation/buffer';
|
|
|
2
2
|
import { normalizeSignature } from '@aztec/foundation/crypto/secp256k1-signer';
|
|
3
3
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
4
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
5
|
+
import type { SigningContext } from '@aztec/validator-ha-signer/types';
|
|
5
6
|
|
|
6
7
|
import type { TypedDataDefinition } from 'viem';
|
|
7
8
|
|
|
@@ -44,9 +45,10 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
|
|
|
44
45
|
/**
|
|
45
46
|
* Sign EIP-712 typed data with all keystore addresses
|
|
46
47
|
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
48
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
47
49
|
* @return signatures
|
|
48
50
|
*/
|
|
49
|
-
public signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
|
|
51
|
+
public signTypedData(typedData: TypedDataDefinition, _context: SigningContext): Promise<Signature[]> {
|
|
50
52
|
return Promise.all(this.addresses.map(address => this.makeJsonRpcSignTypedDataRequest(address, typedData)));
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -54,10 +56,15 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
|
|
|
54
56
|
* Sign EIP-712 typed data with a specific address
|
|
55
57
|
* @param address - The address of the signer to use
|
|
56
58
|
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
59
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
57
60
|
* @returns signature for the specified address
|
|
58
61
|
* @throws Error if the address is not found in the keystore or signing fails
|
|
59
62
|
*/
|
|
60
|
-
public async signTypedDataWithAddress(
|
|
63
|
+
public async signTypedDataWithAddress(
|
|
64
|
+
address: EthAddress,
|
|
65
|
+
typedData: TypedDataDefinition,
|
|
66
|
+
_context: SigningContext,
|
|
67
|
+
): Promise<Signature> {
|
|
61
68
|
if (!this.addresses.some(addr => addr.equals(address))) {
|
|
62
69
|
throw new Error(`Address ${address.toString()} not found in keystore`);
|
|
63
70
|
}
|
|
@@ -69,9 +76,10 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
|
|
|
69
76
|
* Sign a message with all keystore addresses using EIP-191 prefix
|
|
70
77
|
*
|
|
71
78
|
* @param message - The message to sign
|
|
79
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
72
80
|
* @return signatures
|
|
73
81
|
*/
|
|
74
|
-
public signMessage(message: Buffer32): Promise<Signature[]> {
|
|
82
|
+
public signMessage(message: Buffer32, _context: SigningContext): Promise<Signature[]> {
|
|
75
83
|
return Promise.all(this.addresses.map(address => this.makeJsonRpcSignRequest(address, message)));
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -79,10 +87,15 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
|
|
|
79
87
|
* Sign a message with a specific address using EIP-191 prefix
|
|
80
88
|
* @param address - The address of the signer to use
|
|
81
89
|
* @param message - The message to sign
|
|
90
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
82
91
|
* @returns signature for the specified address
|
|
83
92
|
* @throws Error if the address is not found in the keystore or signing fails
|
|
84
93
|
*/
|
|
85
|
-
public async signMessageWithAddress(
|
|
94
|
+
public async signMessageWithAddress(
|
|
95
|
+
address: EthAddress,
|
|
96
|
+
message: Buffer32,
|
|
97
|
+
_context: SigningContext,
|
|
98
|
+
): Promise<Signature> {
|
|
86
99
|
if (!this.addresses.some(addr => addr.equals(address))) {
|
|
87
100
|
throw new Error(`Address ${address.toString()} not found in keystore`);
|
|
88
101
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
GasTxValidator,
|
|
11
11
|
MetadataTxValidator,
|
|
12
12
|
PhasesTxValidator,
|
|
13
|
+
SizeTxValidator,
|
|
13
14
|
TimestampTxValidator,
|
|
14
15
|
TxPermittedValidator,
|
|
15
16
|
TxProofValidator,
|
|
@@ -55,6 +56,7 @@ export function createValidatorForAcceptingTxs(
|
|
|
55
56
|
): TxValidator<Tx> {
|
|
56
57
|
const validators: TxValidator<Tx>[] = [
|
|
57
58
|
new TxPermittedValidator(txsPermitted),
|
|
59
|
+
new SizeTxValidator(),
|
|
58
60
|
new DataTxValidator(),
|
|
59
61
|
new MetadataTxValidator({
|
|
60
62
|
l1ChainId: new Fr(l1ChainId),
|
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';
|
|
@@ -16,7 +22,8 @@ import type { P2P, PeerId, TxProvider } 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,
|
|
22
29
|
Validator,
|
|
@@ -36,6 +43,8 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
|
36
43
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
37
44
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
38
45
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
46
|
+
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
47
|
+
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
39
48
|
|
|
40
49
|
import { EventEmitter } from 'events';
|
|
41
50
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -43,6 +52,8 @@ import type { TypedDataDefinition } from 'viem';
|
|
|
43
52
|
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
44
53
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
45
54
|
import { ValidationService } from './duties/validation_service.js';
|
|
55
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
56
|
+
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
46
57
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
47
58
|
import { ValidatorMetrics } from './metrics.js';
|
|
48
59
|
|
|
@@ -82,7 +93,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
82
93
|
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
83
94
|
|
|
84
95
|
protected constructor(
|
|
85
|
-
private keyStore:
|
|
96
|
+
private keyStore: ExtendedValidatorKeyStore,
|
|
86
97
|
private epochCache: EpochCache,
|
|
87
98
|
private p2pClient: P2P,
|
|
88
99
|
private blockProposalHandler: BlockProposalHandler,
|
|
@@ -165,7 +176,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
static new(
|
|
179
|
+
static async new(
|
|
169
180
|
config: ValidatorClientFullConfig,
|
|
170
181
|
checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
171
182
|
worldState: WorldStateSynchronizer,
|
|
@@ -190,14 +201,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
190
201
|
l1ToL2MessageSource,
|
|
191
202
|
txProvider,
|
|
192
203
|
blockProposalValidator,
|
|
204
|
+
epochCache,
|
|
193
205
|
config,
|
|
194
206
|
metrics,
|
|
195
207
|
dateProvider,
|
|
196
208
|
telemetry,
|
|
197
209
|
);
|
|
198
210
|
|
|
211
|
+
let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
212
|
+
if (config.haSigningEnabled) {
|
|
213
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
214
|
+
const haConfig = {
|
|
215
|
+
...config,
|
|
216
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
217
|
+
};
|
|
218
|
+
const { signer } = await createHASigner(haConfig);
|
|
219
|
+
validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
const validator = new ValidatorClient(
|
|
200
|
-
|
|
223
|
+
validatorKeyStore,
|
|
201
224
|
epochCache,
|
|
202
225
|
p2pClient,
|
|
203
226
|
blockProposalHandler,
|
|
@@ -224,8 +247,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
224
247
|
return this.blockProposalHandler;
|
|
225
248
|
}
|
|
226
249
|
|
|
227
|
-
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
228
|
-
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
250
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
251
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
229
252
|
}
|
|
230
253
|
|
|
231
254
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
@@ -250,6 +273,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
250
273
|
return;
|
|
251
274
|
}
|
|
252
275
|
|
|
276
|
+
await this.keyStore.start();
|
|
277
|
+
|
|
253
278
|
await this.registerHandlers();
|
|
254
279
|
|
|
255
280
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -265,6 +290,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
265
290
|
|
|
266
291
|
public async stop() {
|
|
267
292
|
await this.epochCacheUpdateLoop.stop();
|
|
293
|
+
await this.keyStore.stop();
|
|
268
294
|
}
|
|
269
295
|
|
|
270
296
|
/** Register handlers on the p2p client */
|
|
@@ -301,6 +327,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
301
327
|
*/
|
|
302
328
|
async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
303
329
|
const slotNumber = proposal.slotNumber;
|
|
330
|
+
|
|
331
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
332
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
333
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
334
|
+
|
|
304
335
|
const proposer = proposal.getSender();
|
|
305
336
|
|
|
306
337
|
// Reject proposals with invalid signatures
|
|
@@ -334,7 +365,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
334
365
|
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
335
366
|
proposal,
|
|
336
367
|
proposalSender,
|
|
337
|
-
!!shouldReexecute,
|
|
368
|
+
!!shouldReexecute && !escapeHatchOpen,
|
|
338
369
|
);
|
|
339
370
|
|
|
340
371
|
if (!validationResult.isValid) {
|
|
@@ -359,6 +390,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
359
390
|
|
|
360
391
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
361
392
|
if (
|
|
393
|
+
!escapeHatchOpen &&
|
|
362
394
|
validationResult.reason &&
|
|
363
395
|
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
364
396
|
slashBroadcastedInvalidBlockPenalty > 0n
|
|
@@ -373,8 +405,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
373
405
|
...proposalInfo,
|
|
374
406
|
inCommittee: partOfCommittee,
|
|
375
407
|
fishermanMode: this.config.fishermanMode || false,
|
|
408
|
+
escapeHatchOpen,
|
|
376
409
|
});
|
|
377
410
|
|
|
411
|
+
if (escapeHatchOpen) {
|
|
412
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
378
416
|
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
379
417
|
// Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
|
|
380
418
|
this.validatedBlockSlots.add(slotNumber);
|
|
@@ -395,6 +433,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
395
433
|
const slotNumber = proposal.slotNumber;
|
|
396
434
|
const proposer = proposal.getSender();
|
|
397
435
|
|
|
436
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
437
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
|
|
438
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
398
442
|
// Reject proposals with invalid signatures
|
|
399
443
|
if (!proposer) {
|
|
400
444
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
@@ -531,16 +575,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
575
|
return { isValid: false, reason: 'last_block_not_found' };
|
|
532
576
|
}
|
|
533
577
|
|
|
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
578
|
// Get all full blocks for the slot and checkpoint
|
|
543
|
-
const blocks = await this.getBlocksForSlot(slot
|
|
579
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
544
580
|
if (blocks.length === 0) {
|
|
545
581
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
546
582
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
@@ -554,10 +590,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
554
590
|
// Get checkpoint constants from first block
|
|
555
591
|
const firstBlock = blocks[0];
|
|
556
592
|
const constants = this.extractCheckpointConstants(firstBlock);
|
|
593
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
557
594
|
|
|
558
595
|
// Get L1-to-L2 messages for this checkpoint
|
|
559
596
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
560
597
|
|
|
598
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
599
|
+
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
600
|
+
// actual checkpoints and the blocks/txs in them.
|
|
601
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
602
|
+
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
603
|
+
.filter(b => b.number < checkpointNumber)
|
|
604
|
+
.sort((a, b) => a.number - b.number);
|
|
605
|
+
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
606
|
+
|
|
561
607
|
// Fork world state at the block before the first block
|
|
562
608
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
563
609
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
@@ -568,6 +614,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
568
614
|
checkpointNumber,
|
|
569
615
|
constants,
|
|
570
616
|
l1ToL2Messages,
|
|
617
|
+
previousCheckpointOutHashes,
|
|
571
618
|
fork,
|
|
572
619
|
blocks,
|
|
573
620
|
);
|
|
@@ -595,6 +642,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
595
642
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
596
643
|
}
|
|
597
644
|
|
|
645
|
+
// Check that the accumulated out hash matches the value in the proposal.
|
|
646
|
+
const computedOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
647
|
+
const proposalOutHash = proposal.checkpointHeader.epochOutHash;
|
|
648
|
+
if (!computedOutHash.equals(proposalOutHash)) {
|
|
649
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
650
|
+
proposalOutHash: proposalOutHash.toString(),
|
|
651
|
+
computedOutHash: computedOutHash.toString(),
|
|
652
|
+
...proposalInfo,
|
|
653
|
+
});
|
|
654
|
+
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
655
|
+
}
|
|
656
|
+
|
|
598
657
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
599
658
|
return { isValid: true };
|
|
600
659
|
} finally {
|
|
@@ -602,50 +661,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
602
661
|
}
|
|
603
662
|
}
|
|
604
663
|
|
|
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
664
|
/**
|
|
646
665
|
* Extract checkpoint global variables from a block.
|
|
647
666
|
*/
|
|
648
|
-
private extractCheckpointConstants(block:
|
|
667
|
+
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
649
668
|
const gv = block.header.globalVariables;
|
|
650
669
|
return {
|
|
651
670
|
chainId: gv.chainId,
|
|
@@ -668,14 +687,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
668
687
|
return;
|
|
669
688
|
}
|
|
670
689
|
|
|
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);
|
|
690
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
679
691
|
if (blocks.length === 0) {
|
|
680
692
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
681
693
|
return;
|
|
@@ -722,7 +734,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
722
734
|
|
|
723
735
|
async createBlockProposal(
|
|
724
736
|
blockHeader: BlockHeader,
|
|
725
|
-
indexWithinCheckpoint:
|
|
737
|
+
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
726
738
|
inHash: Fr,
|
|
727
739
|
archive: Fr,
|
|
728
740
|
txs: Tx[],
|
|
@@ -778,8 +790,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
778
790
|
async signAttestationsAndSigners(
|
|
779
791
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
780
792
|
proposer: EthAddress,
|
|
793
|
+
slot: SlotNumber,
|
|
794
|
+
blockNumber: BlockNumber | CheckpointNumber,
|
|
781
795
|
): Promise<Signature> {
|
|
782
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
796
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
|
|
783
797
|
}
|
|
784
798
|
|
|
785
799
|
async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
|
|
@@ -886,7 +900,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
886
900
|
}
|
|
887
901
|
|
|
888
902
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
889
|
-
|
|
903
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
904
|
+
const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
|
|
905
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
890
906
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
891
907
|
return authResponse.toBuffer();
|
|
892
908
|
}
|