@aztec/validator-client 0.0.1-commit.2eb6648a → 0.0.1-commit.343b43af6
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 +54 -10
- package/dest/block_proposal_handler.d.ts +5 -4
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +122 -62
- package/dest/checkpoint_builder.d.ts +15 -5
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +80 -25
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +26 -1
- package/dest/duties/validation_service.d.ts +2 -2
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +5 -11
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +2 -1
- package/dest/index.d.ts +1 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +0 -1
- package/dest/key_store/ha_key_store.js +1 -1
- package/dest/metrics.d.ts +9 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/validator.d.ts +30 -8
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +171 -33
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +149 -80
- package/src/checkpoint_builder.ts +90 -14
- package/src/config.ts +26 -1
- package/src/duties/validation_service.ts +11 -10
- package/src/factory.ts +1 -0
- package/src/index.ts +0 -1
- package/src/key_store/ha_key_store.ts +1 -1
- package/src/metrics.ts +18 -0
- package/src/validator.ts +216 -39
- package/dest/tx_validator/index.d.ts +0 -3
- package/dest/tx_validator/index.d.ts.map +0 -1
- package/dest/tx_validator/index.js +0 -2
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -54
- package/src/tx_validator/index.ts +0 -2
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -154
package/src/config.ts
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
secretValueConfigHelper,
|
|
7
7
|
} from '@aztec/foundation/config';
|
|
8
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
|
+
import { localSignerConfigMappings, validatorHASignerConfigMappings } from '@aztec/stdlib/ha-signing';
|
|
9
10
|
import type { ValidatorClientConfig } from '@aztec/stdlib/interfaces/server';
|
|
10
|
-
import { validatorHASignerConfigMappings } from '@aztec/validator-ha-signer/config';
|
|
11
11
|
|
|
12
12
|
export type { ValidatorClientConfig };
|
|
13
13
|
|
|
@@ -73,6 +73,31 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
|
|
|
73
73
|
description: 'Skip pushing re-executed blocks to archiver (default: false)',
|
|
74
74
|
defaultValue: false,
|
|
75
75
|
},
|
|
76
|
+
attestToEquivocatedProposals: {
|
|
77
|
+
description: 'Agree to attest to equivocated checkpoint proposals (for testing purposes only)',
|
|
78
|
+
...booleanConfigHelper(false),
|
|
79
|
+
},
|
|
80
|
+
validateMaxL2BlockGas: {
|
|
81
|
+
env: 'VALIDATOR_MAX_L2_BLOCK_GAS',
|
|
82
|
+
description: 'Maximum L2 block gas for validation. Proposals exceeding this limit are rejected.',
|
|
83
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
84
|
+
},
|
|
85
|
+
validateMaxDABlockGas: {
|
|
86
|
+
env: 'VALIDATOR_MAX_DA_BLOCK_GAS',
|
|
87
|
+
description: 'Maximum DA block gas for validation. Proposals exceeding this limit are rejected.',
|
|
88
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
89
|
+
},
|
|
90
|
+
validateMaxTxsPerBlock: {
|
|
91
|
+
env: 'VALIDATOR_MAX_TX_PER_BLOCK',
|
|
92
|
+
description: 'Maximum transactions per block for validation. Proposals exceeding this limit are rejected.',
|
|
93
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
94
|
+
},
|
|
95
|
+
validateMaxTxsPerCheckpoint: {
|
|
96
|
+
env: 'VALIDATOR_MAX_TX_PER_CHECKPOINT',
|
|
97
|
+
description: 'Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected.',
|
|
98
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
99
|
+
},
|
|
100
|
+
...localSignerConfigMappings,
|
|
76
101
|
...validatorHASignerConfigMappings,
|
|
77
102
|
};
|
|
78
103
|
|
|
@@ -95,6 +95,7 @@ export class ValidationService {
|
|
|
95
95
|
public createCheckpointProposal(
|
|
96
96
|
checkpointHeader: CheckpointHeader,
|
|
97
97
|
archive: Fr,
|
|
98
|
+
feeAssetPriceModifier: bigint,
|
|
98
99
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
99
100
|
proposerAttesterAddress: EthAddress | undefined,
|
|
100
101
|
options: CheckpointProposalOptions,
|
|
@@ -119,7 +120,13 @@ export class ValidationService {
|
|
|
119
120
|
txs: options.publishFullTxs ? lastBlockInfo.txs : undefined,
|
|
120
121
|
};
|
|
121
122
|
|
|
122
|
-
return CheckpointProposal.createProposalFromSigner(
|
|
123
|
+
return CheckpointProposal.createProposalFromSigner(
|
|
124
|
+
checkpointHeader,
|
|
125
|
+
archive,
|
|
126
|
+
feeAssetPriceModifier,
|
|
127
|
+
lastBlock,
|
|
128
|
+
payloadSigner,
|
|
129
|
+
);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/**
|
|
@@ -137,22 +144,16 @@ export class ValidationService {
|
|
|
137
144
|
attestors: EthAddress[],
|
|
138
145
|
): Promise<CheckpointAttestation[]> {
|
|
139
146
|
// Create the attestation payload from the checkpoint proposal
|
|
140
|
-
const payload = new ConsensusPayload(proposal.checkpointHeader, proposal.archive);
|
|
147
|
+
const payload = new ConsensusPayload(proposal.checkpointHeader, proposal.archive, proposal.feeAssetPriceModifier);
|
|
141
148
|
const buf = Buffer32.fromBuffer(
|
|
142
149
|
keccak256(payload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation)),
|
|
143
150
|
);
|
|
144
151
|
|
|
145
152
|
// TODO(spy/ha): Use checkpointNumber instead of blockNumber once CheckpointHeader includes it.
|
|
146
|
-
//
|
|
153
|
+
// CheckpointProposalCore doesn't have lastBlock info, so use 0 as a proxy.
|
|
147
154
|
// blockNumber is NOT used for the primary key so it's safe to use here.
|
|
148
155
|
// See CheckpointHeader TODO and SigningContext types documentation.
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
blockNumber = proposal.blockNumber;
|
|
152
|
-
} catch {
|
|
153
|
-
// Checkpoint proposal may not have lastBlock, use 0 as fallback
|
|
154
|
-
blockNumber = BlockNumber(0);
|
|
155
|
-
}
|
|
156
|
+
const blockNumber = BlockNumber(0);
|
|
156
157
|
const context: SigningContext = {
|
|
157
158
|
slot: proposal.slotNumber,
|
|
158
159
|
blockNumber,
|
package/src/factory.ts
CHANGED
|
@@ -29,6 +29,7 @@ export function createBlockProposalHandler(
|
|
|
29
29
|
const metrics = new ValidatorMetrics(deps.telemetry);
|
|
30
30
|
const blockProposalValidator = new BlockProposalValidator(deps.epochCache, {
|
|
31
31
|
txsPermitted: !config.disableTransactions,
|
|
32
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
32
33
|
});
|
|
33
34
|
return new BlockProposalHandler(
|
|
34
35
|
deps.checkpointsBuilder,
|
package/src/index.ts
CHANGED
|
@@ -240,7 +240,7 @@ export class HAKeyStore implements ExtendedValidatorKeyStore {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
if (error instanceof SlashingProtectionError) {
|
|
243
|
-
this.log.
|
|
243
|
+
this.log.info(`Duty already signed by another node with different payload`, {
|
|
244
244
|
dutyType: context.dutyType,
|
|
245
245
|
slot: context.slot,
|
|
246
246
|
existingMessageHash: error.existingMessageHash,
|
package/src/metrics.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { EpochNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
1
3
|
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
2
4
|
import {
|
|
3
5
|
Attributes,
|
|
@@ -16,6 +18,8 @@ export class ValidatorMetrics {
|
|
|
16
18
|
private successfulAttestationsCount: UpDownCounter;
|
|
17
19
|
private failedAttestationsBadProposalCount: UpDownCounter;
|
|
18
20
|
private failedAttestationsNodeIssueCount: UpDownCounter;
|
|
21
|
+
private currentEpoch: Gauge;
|
|
22
|
+
private attestedEpochCount: UpDownCounter;
|
|
19
23
|
|
|
20
24
|
private reexMana: Histogram;
|
|
21
25
|
private reexTx: Histogram;
|
|
@@ -64,6 +68,10 @@ export class ValidatorMetrics {
|
|
|
64
68
|
},
|
|
65
69
|
);
|
|
66
70
|
|
|
71
|
+
this.currentEpoch = meter.createGauge(Metrics.VALIDATOR_CURRENT_EPOCH);
|
|
72
|
+
|
|
73
|
+
this.attestedEpochCount = createUpDownCounterWithDefault(meter, Metrics.VALIDATOR_ATTESTED_EPOCH_COUNT);
|
|
74
|
+
|
|
67
75
|
this.reexMana = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_MANA);
|
|
68
76
|
|
|
69
77
|
this.reexTx = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_TX_COUNT);
|
|
@@ -110,4 +118,14 @@ export class ValidatorMetrics {
|
|
|
110
118
|
[Attributes.IS_COMMITTEE_MEMBER]: inCommittee,
|
|
111
119
|
});
|
|
112
120
|
}
|
|
121
|
+
|
|
122
|
+
/** Update the gauge tracking the current epoch number (proxy for total epochs elapsed). */
|
|
123
|
+
public setCurrentEpoch(epoch: EpochNumber) {
|
|
124
|
+
this.currentEpoch.record(Number(epoch));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Increment the count of epochs in which the given attester submitted at least one attestation. */
|
|
128
|
+
public incAttestedEpochCount(attester: EthAddress) {
|
|
129
|
+
this.attestedEpochCount.add(1, { [Attributes.ATTESTER_ADDRESS]: attester.toString() });
|
|
130
|
+
}
|
|
113
131
|
}
|
package/src/validator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
4
5
|
import {
|
|
5
6
|
BlockNumber,
|
|
6
7
|
CheckpointNumber,
|
|
@@ -18,12 +19,13 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
18
19
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
20
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
21
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
23
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
24
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
25
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
26
|
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
-
import {
|
|
27
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
28
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
29
|
import type {
|
|
28
30
|
CreateCheckpointProposalLastBlockData,
|
|
29
31
|
ITxProvider,
|
|
@@ -32,20 +34,21 @@ import type {
|
|
|
32
34
|
WorldStateSynchronizer,
|
|
33
35
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
36
|
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
38
|
-
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
import {
|
|
38
|
+
type BlockProposal,
|
|
39
|
+
type BlockProposalOptions,
|
|
40
|
+
type CheckpointAttestation,
|
|
41
|
+
CheckpointProposal,
|
|
42
|
+
type CheckpointProposalCore,
|
|
43
|
+
type CheckpointProposalOptions,
|
|
41
44
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
45
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
46
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
45
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
46
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
-
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
49
|
+
import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
|
|
48
50
|
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
51
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
49
52
|
|
|
50
53
|
import { EventEmitter } from 'events';
|
|
51
54
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -76,18 +79,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
79
|
private validationService: ValidationService;
|
|
77
80
|
private metrics: ValidatorMetrics;
|
|
78
81
|
private log: Logger;
|
|
79
|
-
|
|
80
82
|
// Whether it has already registered handlers on the p2p client
|
|
81
83
|
private hasRegisteredHandlers = false;
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
86
|
+
private lastProposedBlock?: BlockProposal;
|
|
87
|
+
|
|
88
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
89
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
90
|
|
|
86
91
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
92
|
private epochCacheUpdateLoop: RunningPromise;
|
|
93
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
94
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
88
95
|
|
|
89
96
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
97
|
|
|
98
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
99
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
100
|
+
|
|
91
101
|
protected constructor(
|
|
92
102
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
103
|
private epochCache: EpochCache,
|
|
@@ -99,6 +109,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
99
109
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
100
110
|
private config: ValidatorClientFullConfig,
|
|
101
111
|
private blobClient: BlobClientInterface,
|
|
112
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
102
113
|
private dateProvider: DateProvider = new DateProvider(),
|
|
103
114
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
104
115
|
log = createLogger('validator'),
|
|
@@ -152,6 +163,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
152
163
|
this.log.trace(`No committee found for slot`);
|
|
153
164
|
return;
|
|
154
165
|
}
|
|
166
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
155
167
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
156
168
|
const me = this.getValidatorAddresses();
|
|
157
169
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -189,6 +201,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
189
201
|
const metrics = new ValidatorMetrics(telemetry);
|
|
190
202
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
191
203
|
txsPermitted: !config.disableTransactions,
|
|
204
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
192
205
|
});
|
|
193
206
|
const blockProposalHandler = new BlockProposalHandler(
|
|
194
207
|
checkpointsBuilder,
|
|
@@ -204,16 +217,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
204
217
|
telemetry,
|
|
205
218
|
);
|
|
206
219
|
|
|
207
|
-
|
|
220
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
221
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
208
222
|
if (config.haSigningEnabled) {
|
|
223
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
209
224
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
225
|
const haConfig = {
|
|
211
226
|
...config,
|
|
212
227
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
228
|
};
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
230
|
+
telemetryClient: telemetry,
|
|
231
|
+
dateProvider,
|
|
232
|
+
}));
|
|
233
|
+
} else {
|
|
234
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
235
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
236
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
237
|
+
telemetryClient: telemetry,
|
|
238
|
+
dateProvider,
|
|
239
|
+
}));
|
|
216
240
|
}
|
|
241
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
217
242
|
|
|
218
243
|
const validator = new ValidatorClient(
|
|
219
244
|
validatorKeyStore,
|
|
@@ -226,6 +251,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
226
251
|
l1ToL2MessageSource,
|
|
227
252
|
config,
|
|
228
253
|
blobClient,
|
|
254
|
+
slashingProtectionSigner,
|
|
229
255
|
dateProvider,
|
|
230
256
|
telemetry,
|
|
231
257
|
);
|
|
@@ -263,6 +289,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
263
289
|
this.config = { ...this.config, ...config };
|
|
264
290
|
}
|
|
265
291
|
|
|
292
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
293
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
294
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
295
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
296
|
+
}
|
|
297
|
+
|
|
266
298
|
public async start() {
|
|
267
299
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
268
300
|
this.log.warn(`Validator client already started`);
|
|
@@ -314,6 +346,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
314
346
|
this.handleDuplicateProposal(info);
|
|
315
347
|
});
|
|
316
348
|
|
|
349
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
350
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
351
|
+
this.handleDuplicateAttestation(info);
|
|
352
|
+
});
|
|
353
|
+
|
|
317
354
|
const myAddresses = this.getValidatorAddresses();
|
|
318
355
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
319
356
|
|
|
@@ -341,6 +378,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
341
378
|
return false;
|
|
342
379
|
}
|
|
343
380
|
|
|
381
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
382
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
383
|
+
this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
384
|
+
proposer: proposer.toString(),
|
|
385
|
+
slotNumber,
|
|
386
|
+
});
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
344
390
|
// Check if we're in the committee (for metrics purposes)
|
|
345
391
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
346
392
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -370,9 +416,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
370
416
|
);
|
|
371
417
|
|
|
372
418
|
if (!validationResult.isValid) {
|
|
373
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
374
|
-
|
|
375
419
|
const reason = validationResult.reason || 'unknown';
|
|
420
|
+
|
|
421
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
422
|
+
|
|
376
423
|
// Classify failure reason: bad proposal vs node issue
|
|
377
424
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
378
425
|
'invalid_proposal',
|
|
@@ -442,6 +489,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
442
489
|
return undefined;
|
|
443
490
|
}
|
|
444
491
|
|
|
492
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
493
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
494
|
+
this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
495
|
+
proposer: proposer.toString(),
|
|
496
|
+
slotNumber,
|
|
497
|
+
});
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Validate fee asset price modifier is within allowed range
|
|
502
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
503
|
+
this.log.warn(
|
|
504
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
|
|
505
|
+
);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
445
509
|
// Check that I have any address in current committee before attesting
|
|
446
510
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
447
511
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -450,11 +514,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
450
514
|
slotNumber,
|
|
451
515
|
archive: proposal.archive.toString(),
|
|
452
516
|
proposer: proposer.toString(),
|
|
453
|
-
txCount: proposal.txHashes.length,
|
|
454
517
|
};
|
|
455
518
|
this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
|
|
456
519
|
...proposalInfo,
|
|
457
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
458
520
|
fishermanMode: this.config.fishermanMode || false,
|
|
459
521
|
});
|
|
460
522
|
|
|
@@ -490,6 +552,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
490
552
|
|
|
491
553
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
492
554
|
|
|
555
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
556
|
+
const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
557
|
+
for (const attester of inCommittee) {
|
|
558
|
+
const key = attester.toString();
|
|
559
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
560
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
561
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
562
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
493
566
|
// Determine which validators should attest
|
|
494
567
|
let attestors: EthAddress[];
|
|
495
568
|
if (partOfCommittee) {
|
|
@@ -515,14 +588,44 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
515
588
|
return undefined;
|
|
516
589
|
}
|
|
517
590
|
|
|
518
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
591
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
596
|
+
* @returns true if we should attest, false if we should skip
|
|
597
|
+
*/
|
|
598
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
599
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
600
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Check if incoming slot is strictly greater than last attested
|
|
605
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
606
|
+
this.log.warn(
|
|
607
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
608
|
+
);
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return true;
|
|
519
613
|
}
|
|
520
614
|
|
|
521
615
|
private async createCheckpointAttestationsFromProposal(
|
|
522
616
|
proposal: CheckpointProposalCore,
|
|
523
617
|
attestors: EthAddress[] = [],
|
|
524
|
-
): Promise<CheckpointAttestation[]> {
|
|
618
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
619
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
620
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
525
624
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
625
|
+
|
|
626
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
627
|
+
this.lastAttestedProposal = proposal;
|
|
628
|
+
|
|
526
629
|
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
527
630
|
return attestations;
|
|
528
631
|
}
|
|
@@ -536,7 +639,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
536
639
|
proposalInfo: LogData,
|
|
537
640
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
538
641
|
const slot = proposal.slotNumber;
|
|
539
|
-
|
|
642
|
+
|
|
643
|
+
// Timeout block syncing at the start of the next slot
|
|
644
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
645
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
646
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
540
647
|
|
|
541
648
|
// Wait for last block to sync by archive
|
|
542
649
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -571,6 +678,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
571
678
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
572
679
|
}
|
|
573
680
|
|
|
681
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
682
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
683
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
684
|
+
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
685
|
+
}
|
|
686
|
+
|
|
574
687
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
575
688
|
...proposalInfo,
|
|
576
689
|
blockNumbers: blocks.map(b => b.number),
|
|
@@ -584,14 +697,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
584
697
|
// Get L1-to-L2 messages for this checkpoint
|
|
585
698
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
586
699
|
|
|
587
|
-
//
|
|
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.
|
|
700
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
590
701
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
591
|
-
const
|
|
592
|
-
.filter(
|
|
593
|
-
.
|
|
594
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
702
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
703
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
704
|
+
.map(c => c.checkpointOutHash);
|
|
595
705
|
|
|
596
706
|
// Fork world state at the block before the first block
|
|
597
707
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
@@ -602,6 +712,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
602
712
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
603
713
|
checkpointNumber,
|
|
604
714
|
constants,
|
|
715
|
+
proposal.feeAssetPriceModifier,
|
|
605
716
|
l1ToL2Messages,
|
|
606
717
|
previousCheckpointOutHashes,
|
|
607
718
|
fork,
|
|
@@ -648,6 +759,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
648
759
|
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
649
760
|
}
|
|
650
761
|
|
|
762
|
+
// Final round of validations on the checkpoint, just in case.
|
|
763
|
+
try {
|
|
764
|
+
validateCheckpoint(computedCheckpoint, {
|
|
765
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
766
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
767
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
768
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
769
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
|
|
770
|
+
});
|
|
771
|
+
} catch (err) {
|
|
772
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
773
|
+
return { isValid: false, reason: 'checkpoint_validation_failed' };
|
|
774
|
+
}
|
|
775
|
+
|
|
651
776
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
652
777
|
return { isValid: true };
|
|
653
778
|
} finally {
|
|
@@ -664,6 +789,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
664
789
|
chainId: gv.chainId,
|
|
665
790
|
version: gv.version,
|
|
666
791
|
slotNumber: gv.slotNumber,
|
|
792
|
+
timestamp: gv.timestamp,
|
|
667
793
|
coinbase: gv.coinbase,
|
|
668
794
|
feeRecipient: gv.feeRecipient,
|
|
669
795
|
gasFees: gv.gasFees,
|
|
@@ -673,7 +799,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
673
799
|
/**
|
|
674
800
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
675
801
|
*/
|
|
676
|
-
|
|
802
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
677
803
|
try {
|
|
678
804
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
679
805
|
if (!lastBlockHeader) {
|
|
@@ -688,7 +814,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
688
814
|
}
|
|
689
815
|
|
|
690
816
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
691
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
817
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
692
818
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
693
819
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
694
820
|
...proposalInfo,
|
|
@@ -750,6 +876,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
750
876
|
]);
|
|
751
877
|
}
|
|
752
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
881
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
882
|
+
*/
|
|
883
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
884
|
+
const { slot, attester } = info;
|
|
885
|
+
|
|
886
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
887
|
+
attester: attester.toString(),
|
|
888
|
+
slot,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
892
|
+
{
|
|
893
|
+
validator: attester,
|
|
894
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
895
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
896
|
+
epochOrSlot: BigInt(slot),
|
|
897
|
+
},
|
|
898
|
+
]);
|
|
899
|
+
}
|
|
900
|
+
|
|
753
901
|
async createBlockProposal(
|
|
754
902
|
blockHeader: BlockHeader,
|
|
755
903
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -759,11 +907,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
759
907
|
proposerAddress: EthAddress | undefined,
|
|
760
908
|
options: BlockProposalOptions = {},
|
|
761
909
|
): Promise<BlockProposal> {
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
910
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
911
|
+
if (this.lastProposedBlock) {
|
|
912
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
913
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
914
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
915
|
+
|
|
916
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
917
|
+
throw new Error(
|
|
918
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
919
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
767
923
|
|
|
768
924
|
this.log.info(
|
|
769
925
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -780,25 +936,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
780
936
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
781
937
|
},
|
|
782
938
|
);
|
|
783
|
-
this.
|
|
939
|
+
this.lastProposedBlock = newProposal;
|
|
784
940
|
return newProposal;
|
|
785
941
|
}
|
|
786
942
|
|
|
787
943
|
async createCheckpointProposal(
|
|
788
944
|
checkpointHeader: CheckpointHeader,
|
|
789
945
|
archive: Fr,
|
|
946
|
+
feeAssetPriceModifier: bigint,
|
|
790
947
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
791
948
|
proposerAddress: EthAddress | undefined,
|
|
792
949
|
options: CheckpointProposalOptions = {},
|
|
793
950
|
): Promise<CheckpointProposal> {
|
|
951
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
952
|
+
if (this.lastProposedCheckpoint) {
|
|
953
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
954
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
955
|
+
|
|
956
|
+
if (newSlot <= lastSlot) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
959
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
794
964
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
795
|
-
|
|
965
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
796
966
|
checkpointHeader,
|
|
797
967
|
archive,
|
|
968
|
+
feeAssetPriceModifier,
|
|
798
969
|
lastBlockInfo,
|
|
799
970
|
proposerAddress,
|
|
800
971
|
options,
|
|
801
972
|
);
|
|
973
|
+
this.lastProposedCheckpoint = newProposal;
|
|
974
|
+
return newProposal;
|
|
802
975
|
}
|
|
803
976
|
|
|
804
977
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -820,6 +993,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
820
993
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
821
994
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
822
995
|
|
|
996
|
+
if (!attestations) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
|
|
823
1000
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
824
1001
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
825
1002
|
// due to inactivity for missed attestations.
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export * from './nullifier_cache.js';
|
|
2
|
-
export * from './tx_validator_factory.js';
|
|
3
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90eF92YWxpZGF0b3IvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDJCQUEyQixDQUFDIn0=
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tx_validator/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC"}
|