@aztec/validator-client 0.0.1-commit.6d3c34e → 0.0.1-commit.9372f48
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 +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 +13 -13
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +82 -80
- 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 +98 -93
|
@@ -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 { 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 */
|
|
@@ -301,6 +323,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
301
323
|
*/
|
|
302
324
|
async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
303
325
|
const slotNumber = proposal.slotNumber;
|
|
326
|
+
|
|
327
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
328
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
329
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
330
|
+
|
|
304
331
|
const proposer = proposal.getSender();
|
|
305
332
|
|
|
306
333
|
// Reject proposals with invalid signatures
|
|
@@ -334,7 +361,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
334
361
|
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
335
362
|
proposal,
|
|
336
363
|
proposalSender,
|
|
337
|
-
!!shouldReexecute,
|
|
364
|
+
!!shouldReexecute && !escapeHatchOpen,
|
|
338
365
|
);
|
|
339
366
|
|
|
340
367
|
if (!validationResult.isValid) {
|
|
@@ -359,6 +386,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
359
386
|
|
|
360
387
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
361
388
|
if (
|
|
389
|
+
!escapeHatchOpen &&
|
|
362
390
|
validationResult.reason &&
|
|
363
391
|
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
364
392
|
slashBroadcastedInvalidBlockPenalty > 0n
|
|
@@ -373,11 +401,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
373
401
|
...proposalInfo,
|
|
374
402
|
inCommittee: partOfCommittee,
|
|
375
403
|
fishermanMode: this.config.fishermanMode || false,
|
|
404
|
+
escapeHatchOpen,
|
|
376
405
|
});
|
|
377
406
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
407
|
+
if (escapeHatchOpen) {
|
|
408
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
381
411
|
|
|
382
412
|
return true;
|
|
383
413
|
}
|
|
@@ -395,6 +425,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
395
425
|
const slotNumber = proposal.slotNumber;
|
|
396
426
|
const proposer = proposal.getSender();
|
|
397
427
|
|
|
428
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
429
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
|
|
430
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
398
434
|
// Reject proposals with invalid signatures
|
|
399
435
|
if (!proposer) {
|
|
400
436
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
@@ -417,17 +453,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
417
453
|
fishermanMode: this.config.fishermanMode || false,
|
|
418
454
|
});
|
|
419
455
|
|
|
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
456
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
457
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
458
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
431
459
|
} else {
|
|
432
460
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
433
461
|
if (!validationResult.isValid) {
|
|
@@ -503,7 +531,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
531
|
proposalInfo: LogData,
|
|
504
532
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
505
533
|
const slot = proposal.slotNumber;
|
|
506
|
-
const timeoutSeconds = 10;
|
|
534
|
+
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
507
535
|
|
|
508
536
|
// Wait for last block to sync by archive
|
|
509
537
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -531,16 +559,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
559
|
return { isValid: false, reason: 'last_block_not_found' };
|
|
532
560
|
}
|
|
533
561
|
|
|
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
562
|
// Get all full blocks for the slot and checkpoint
|
|
543
|
-
const blocks = await this.getBlocksForSlot(slot
|
|
563
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
544
564
|
if (blocks.length === 0) {
|
|
545
565
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
546
566
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
@@ -554,10 +574,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
554
574
|
// Get checkpoint constants from first block
|
|
555
575
|
const firstBlock = blocks[0];
|
|
556
576
|
const constants = this.extractCheckpointConstants(firstBlock);
|
|
577
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
557
578
|
|
|
558
579
|
// Get L1-to-L2 messages for this checkpoint
|
|
559
580
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
560
581
|
|
|
582
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
583
|
+
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
584
|
+
// actual checkpoints and the blocks/txs in them.
|
|
585
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
586
|
+
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
587
|
+
.filter(b => b.number < checkpointNumber)
|
|
588
|
+
.sort((a, b) => a.number - b.number);
|
|
589
|
+
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
590
|
+
|
|
561
591
|
// Fork world state at the block before the first block
|
|
562
592
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
563
593
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
@@ -568,8 +598,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
568
598
|
checkpointNumber,
|
|
569
599
|
constants,
|
|
570
600
|
l1ToL2Messages,
|
|
601
|
+
previousCheckpointOutHashes,
|
|
571
602
|
fork,
|
|
572
603
|
blocks,
|
|
604
|
+
this.log.getBindings(),
|
|
573
605
|
);
|
|
574
606
|
|
|
575
607
|
// Complete the checkpoint to get computed values
|
|
@@ -595,6 +627,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
595
627
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
596
628
|
}
|
|
597
629
|
|
|
630
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
631
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
632
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
633
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
634
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
635
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
636
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
637
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
638
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
639
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
640
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
641
|
+
...proposalInfo,
|
|
642
|
+
});
|
|
643
|
+
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
644
|
+
}
|
|
645
|
+
|
|
598
646
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
599
647
|
return { isValid: true };
|
|
600
648
|
} finally {
|
|
@@ -602,50 +650,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
602
650
|
}
|
|
603
651
|
}
|
|
604
652
|
|
|
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
653
|
/**
|
|
646
654
|
* Extract checkpoint global variables from a block.
|
|
647
655
|
*/
|
|
648
|
-
private extractCheckpointConstants(block:
|
|
656
|
+
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
649
657
|
const gv = block.header.globalVariables;
|
|
650
658
|
return {
|
|
651
659
|
chainId: gv.chainId,
|
|
@@ -668,14 +676,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
668
676
|
return;
|
|
669
677
|
}
|
|
670
678
|
|
|
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);
|
|
679
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
679
680
|
if (blocks.length === 0) {
|
|
680
681
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
681
682
|
return;
|
|
@@ -722,12 +723,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
722
723
|
|
|
723
724
|
async createBlockProposal(
|
|
724
725
|
blockHeader: BlockHeader,
|
|
725
|
-
indexWithinCheckpoint:
|
|
726
|
+
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
726
727
|
inHash: Fr,
|
|
727
728
|
archive: Fr,
|
|
728
729
|
txs: Tx[],
|
|
729
730
|
proposerAddress: EthAddress | undefined,
|
|
730
|
-
options: BlockProposalOptions,
|
|
731
|
+
options: BlockProposalOptions = {},
|
|
731
732
|
): Promise<BlockProposal> {
|
|
732
733
|
// TODO(palla/mbps): Prevent double proposals properly
|
|
733
734
|
// if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
|
|
@@ -759,7 +760,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
759
760
|
archive: Fr,
|
|
760
761
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
761
762
|
proposerAddress: EthAddress | undefined,
|
|
762
|
-
options: CheckpointProposalOptions,
|
|
763
|
+
options: CheckpointProposalOptions = {},
|
|
763
764
|
): Promise<CheckpointProposal> {
|
|
764
765
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
765
766
|
return await this.validationService.createCheckpointProposal(
|
|
@@ -778,8 +779,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
778
779
|
async signAttestationsAndSigners(
|
|
779
780
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
780
781
|
proposer: EthAddress,
|
|
782
|
+
slot: SlotNumber,
|
|
783
|
+
blockNumber: BlockNumber | CheckpointNumber,
|
|
781
784
|
): Promise<Signature> {
|
|
782
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
785
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
|
|
783
786
|
}
|
|
784
787
|
|
|
785
788
|
async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
|
|
@@ -886,7 +889,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
886
889
|
}
|
|
887
890
|
|
|
888
891
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
889
|
-
|
|
892
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
893
|
+
const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
|
|
894
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
890
895
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
891
896
|
return authResponse.toBuffer();
|
|
892
897
|
}
|