@aztec/validator-client 0.0.0-test.1 → 0.0.1-commit.b655e406
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/block_proposal_handler.d.ts +52 -0
- package/dest/block_proposal_handler.d.ts.map +1 -0
- package/dest/block_proposal_handler.js +286 -0
- package/dest/config.d.ts +2 -13
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +31 -7
- package/dest/duties/validation_service.d.ts +16 -8
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +35 -11
- package/dest/factory.d.ts +21 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +13 -6
- package/dest/index.d.ts +3 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +3 -1
- package/dest/key_store/index.d.ts +2 -0
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +2 -0
- package/dest/key_store/interface.d.ts +54 -5
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/interface.js +3 -3
- package/dest/key_store/local_key_store.d.ts +40 -10
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +63 -16
- package/dest/key_store/node_keystore_adapter.d.ts +138 -0
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -0
- package/dest/key_store/node_keystore_adapter.js +316 -0
- package/dest/key_store/web3signer_key_store.d.ts +67 -0
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -0
- package/dest/key_store/web3signer_key_store.js +152 -0
- package/dest/metrics.d.ts +11 -4
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +52 -15
- package/dest/validator.d.ts +48 -61
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +262 -165
- package/package.json +25 -19
- package/src/block_proposal_handler.ts +343 -0
- package/src/config.ts +42 -22
- package/src/duties/validation_service.ts +69 -14
- package/src/factory.ts +56 -11
- package/src/index.ts +3 -1
- package/src/key_store/index.ts +2 -0
- package/src/key_store/interface.ts +61 -5
- package/src/key_store/local_key_store.ts +67 -17
- package/src/key_store/node_keystore_adapter.ts +375 -0
- package/src/key_store/web3signer_key_store.ts +192 -0
- package/src/metrics.ts +68 -17
- package/src/validator.ts +381 -233
- package/dest/errors/index.d.ts +0 -2
- package/dest/errors/index.d.ts.map +0 -1
- package/dest/errors/index.js +0 -1
- package/dest/errors/validator.error.d.ts +0 -29
- package/dest/errors/validator.error.d.ts.map +0 -1
- package/dest/errors/validator.error.js +0 -45
- package/src/errors/index.ts +0 -1
- package/src/errors/validator.error.ts +0 -55
package/src/validator.ts
CHANGED
|
@@ -1,151 +1,239 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
2
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
4
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
5
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
5
6
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
6
7
|
import { sleep } from '@aztec/foundation/sleep';
|
|
7
|
-
import { DateProvider
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import type {
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
import type {
|
|
8
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
|
+
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
10
|
+
import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
|
|
11
|
+
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
12
|
+
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
13
|
+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
14
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSource } from '@aztec/stdlib/block';
|
|
15
|
+
import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server';
|
|
16
|
+
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
17
|
+
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
18
|
+
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
19
|
+
import type { StateReference, Tx } from '@aztec/stdlib/tx';
|
|
20
|
+
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
21
|
+
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
22
|
+
|
|
23
|
+
import { EventEmitter } from 'events';
|
|
24
|
+
import type { TypedDataDefinition } from 'viem';
|
|
25
|
+
|
|
26
|
+
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
16
27
|
import { ValidationService } from './duties/validation_service.js';
|
|
17
|
-
import {
|
|
18
|
-
AttestationTimeoutError,
|
|
19
|
-
BlockBuilderNotProvidedError,
|
|
20
|
-
InvalidValidatorPrivateKeyError,
|
|
21
|
-
ReExFailedTxsError,
|
|
22
|
-
ReExStateMismatchError,
|
|
23
|
-
ReExTimeoutError,
|
|
24
|
-
TransactionsNotAvailableError,
|
|
25
|
-
} from './errors/validator.error.js';
|
|
26
|
-
import type { ValidatorKeyStore } from './key_store/interface.js';
|
|
27
|
-
import { LocalKeyStore } from './key_store/local_key_store.js';
|
|
28
|
+
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
28
29
|
import { ValidatorMetrics } from './metrics.js';
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) => Promise<{
|
|
40
|
-
block: L2Block;
|
|
41
|
-
publicProcessorDuration: number;
|
|
42
|
-
numTxs: number;
|
|
43
|
-
numFailedTxs: number;
|
|
44
|
-
blockBuildingTimer: Timer;
|
|
45
|
-
}>;
|
|
46
|
-
|
|
47
|
-
export interface Validator {
|
|
48
|
-
start(): Promise<void>;
|
|
49
|
-
registerBlockProposalHandler(): void;
|
|
50
|
-
registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
|
|
51
|
-
|
|
52
|
-
// Block validation responsibilities
|
|
53
|
-
createBlockProposal(header: BlockHeader, archive: Fr, txs: TxHash[]): Promise<BlockProposal | undefined>;
|
|
54
|
-
attestToProposal(proposal: BlockProposal): void;
|
|
55
|
-
|
|
56
|
-
broadcastBlockProposal(proposal: BlockProposal): void;
|
|
57
|
-
collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
|
|
58
|
-
}
|
|
31
|
+
// We maintain a set of proposers who have proposed invalid blocks.
|
|
32
|
+
// Just cap the set to avoid unbounded growth.
|
|
33
|
+
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
34
|
+
|
|
35
|
+
// What errors from the block proposal handler result in slashing
|
|
36
|
+
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
37
|
+
'state_mismatch',
|
|
38
|
+
'failed_txs',
|
|
39
|
+
];
|
|
59
40
|
|
|
60
41
|
/**
|
|
61
42
|
* Validator Client
|
|
62
43
|
*/
|
|
63
|
-
export class ValidatorClient extends
|
|
44
|
+
export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter) implements Validator, Watcher {
|
|
45
|
+
public readonly tracer: Tracer;
|
|
64
46
|
private validationService: ValidationService;
|
|
65
47
|
private metrics: ValidatorMetrics;
|
|
66
48
|
|
|
49
|
+
// Whether it has already registered handlers on the p2p client
|
|
50
|
+
private hasRegisteredHandlers = false;
|
|
51
|
+
|
|
67
52
|
// Used to check if we are sending the same proposal twice
|
|
68
53
|
private previousProposal?: BlockProposal;
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
private blockBuilder?: BlockBuilderCallback = undefined;
|
|
72
|
-
|
|
55
|
+
private lastEpochForCommitteeUpdateLoop: bigint | undefined;
|
|
73
56
|
private epochCacheUpdateLoop: RunningPromise;
|
|
74
57
|
|
|
75
|
-
private
|
|
58
|
+
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
76
59
|
|
|
77
|
-
constructor(
|
|
78
|
-
private keyStore:
|
|
60
|
+
protected constructor(
|
|
61
|
+
private keyStore: NodeKeystoreAdapter,
|
|
79
62
|
private epochCache: EpochCache,
|
|
80
63
|
private p2pClient: P2P,
|
|
81
|
-
private
|
|
64
|
+
private blockProposalHandler: BlockProposalHandler,
|
|
65
|
+
private config: ValidatorClientFullConfig,
|
|
82
66
|
private dateProvider: DateProvider = new DateProvider(),
|
|
83
67
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
84
68
|
private log = createLogger('validator'),
|
|
85
69
|
) {
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
super();
|
|
71
|
+
this.tracer = telemetry.getTracer('Validator');
|
|
88
72
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
89
73
|
|
|
90
|
-
this.validationService = new ValidationService(keyStore);
|
|
74
|
+
this.validationService = new ValidationService(keyStore, log.createChild('validation-service'));
|
|
91
75
|
|
|
92
|
-
|
|
76
|
+
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
77
|
+
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
93
78
|
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
this.epochCache
|
|
98
|
-
.getCommittee()
|
|
99
|
-
.then(() => {})
|
|
100
|
-
.catch(err => log.error('Error updating validator committee', err)),
|
|
101
|
-
log,
|
|
102
|
-
1000,
|
|
103
|
-
);
|
|
79
|
+
const myAddresses = this.getValidatorAddresses();
|
|
80
|
+
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
81
|
+
}
|
|
104
82
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
83
|
+
public static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager, logger?: Logger) {
|
|
84
|
+
const validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
85
|
+
const validatorAddresses = validatorKeyStore.getAddresses();
|
|
86
|
+
// Verify that we can retrieve all required data from the key store
|
|
87
|
+
for (const address of validatorAddresses) {
|
|
88
|
+
// Functions throw if required data is not available
|
|
89
|
+
let coinbase: EthAddress;
|
|
90
|
+
let feeRecipient: AztecAddress;
|
|
91
|
+
try {
|
|
92
|
+
coinbase = validatorKeyStore.getCoinbaseAddress(address);
|
|
93
|
+
feeRecipient = validatorKeyStore.getFeeRecipient(address);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(`Failed to retrieve required data for validator address ${address}, error: ${error}`);
|
|
112
96
|
}
|
|
113
|
-
});
|
|
114
97
|
|
|
115
|
-
|
|
98
|
+
const publisherAddresses = validatorKeyStore.getPublisherAddresses(address);
|
|
99
|
+
if (!publisherAddresses.length) {
|
|
100
|
+
throw new Error(`No publisher addresses found for validator address ${address}`);
|
|
101
|
+
}
|
|
102
|
+
logger?.debug(
|
|
103
|
+
`Validator ${address.toString()} configured with coinbase ${coinbase.toString()}, feeRecipient ${feeRecipient.toString()} and publishers ${publisherAddresses.map(x => x.toString()).join()}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async handleEpochCommitteeUpdate() {
|
|
109
|
+
try {
|
|
110
|
+
const { committee, epoch } = await this.epochCache.getCommittee('next');
|
|
111
|
+
if (!committee) {
|
|
112
|
+
this.log.trace(`No committee found for slot`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
116
|
+
const me = this.getValidatorAddresses();
|
|
117
|
+
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
118
|
+
const inCommittee = me.filter(a => committeeSet.has(a.toString()));
|
|
119
|
+
if (inCommittee.length > 0) {
|
|
120
|
+
this.log.info(
|
|
121
|
+
`Validators ${inCommittee.map(a => a.toString()).join(',')} are on the validator committee for epoch ${epoch}`,
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
this.log.verbose(
|
|
125
|
+
`Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
this.lastEpochForCommitteeUpdateLoop = epoch;
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
this.log.error(`Error updating epoch committee`, err);
|
|
132
|
+
}
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
static new(
|
|
119
|
-
config:
|
|
136
|
+
config: ValidatorClientFullConfig,
|
|
137
|
+
blockBuilder: IFullNodeBlockBuilder,
|
|
120
138
|
epochCache: EpochCache,
|
|
121
139
|
p2pClient: P2P,
|
|
140
|
+
blockSource: L2BlockSource,
|
|
141
|
+
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
142
|
+
txProvider: TxProvider,
|
|
143
|
+
keyStoreManager: KeystoreManager,
|
|
122
144
|
dateProvider: DateProvider = new DateProvider(),
|
|
123
145
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
124
146
|
) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
147
|
+
const metrics = new ValidatorMetrics(telemetry);
|
|
148
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
149
|
+
txsPermitted: !config.disableTransactions,
|
|
150
|
+
});
|
|
151
|
+
const blockProposalHandler = new BlockProposalHandler(
|
|
152
|
+
blockBuilder,
|
|
153
|
+
blockSource,
|
|
154
|
+
l1ToL2MessageSource,
|
|
155
|
+
txProvider,
|
|
156
|
+
blockProposalValidator,
|
|
157
|
+
config,
|
|
158
|
+
metrics,
|
|
159
|
+
dateProvider,
|
|
160
|
+
telemetry,
|
|
161
|
+
);
|
|
128
162
|
|
|
129
|
-
const
|
|
130
|
-
|
|
163
|
+
const validator = new ValidatorClient(
|
|
164
|
+
NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
|
|
165
|
+
epochCache,
|
|
166
|
+
p2pClient,
|
|
167
|
+
blockProposalHandler,
|
|
168
|
+
config,
|
|
169
|
+
dateProvider,
|
|
170
|
+
telemetry,
|
|
171
|
+
);
|
|
131
172
|
|
|
132
|
-
const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, config, dateProvider, telemetry);
|
|
133
|
-
validator.registerBlockProposalHandler();
|
|
134
173
|
return validator;
|
|
135
174
|
}
|
|
136
175
|
|
|
176
|
+
public getValidatorAddresses() {
|
|
177
|
+
return this.keyStore
|
|
178
|
+
.getAddresses()
|
|
179
|
+
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public getBlockProposalHandler() {
|
|
183
|
+
return this.blockProposalHandler;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Proxy method for backwards compatibility with tests
|
|
187
|
+
public reExecuteTransactions(
|
|
188
|
+
proposal: BlockProposal,
|
|
189
|
+
blockNumber: number,
|
|
190
|
+
txs: any[],
|
|
191
|
+
l1ToL2Messages: Fr[],
|
|
192
|
+
): Promise<any> {
|
|
193
|
+
return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
197
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
201
|
+
return this.keyStore.getCoinbaseAddress(attestor);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public getFeeRecipientForAttestor(attestor: EthAddress): AztecAddress {
|
|
205
|
+
return this.keyStore.getFeeRecipient(attestor);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public getConfig(): ValidatorClientFullConfig {
|
|
209
|
+
return this.config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
213
|
+
this.config = { ...this.config, ...config };
|
|
214
|
+
}
|
|
215
|
+
|
|
137
216
|
public async start() {
|
|
138
|
-
|
|
139
|
-
|
|
217
|
+
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
218
|
+
this.log.warn(`Validator client already started`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
140
221
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
222
|
+
await this.registerHandlers();
|
|
223
|
+
|
|
224
|
+
const myAddresses = this.getValidatorAddresses();
|
|
225
|
+
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
226
|
+
if (inCommittee.length > 0) {
|
|
227
|
+
this.log.info(
|
|
228
|
+
`Started validator with addresses in current validator committee: ${inCommittee
|
|
229
|
+
.map(a => a.toString())
|
|
230
|
+
.join(', ')}`,
|
|
231
|
+
);
|
|
145
232
|
} else {
|
|
146
|
-
this.log.info(`Started validator with
|
|
233
|
+
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
147
234
|
}
|
|
148
235
|
this.epochCacheUpdateLoop.start();
|
|
236
|
+
|
|
149
237
|
return Promise.resolve();
|
|
150
238
|
}
|
|
151
239
|
|
|
@@ -153,185 +241,221 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
153
241
|
await this.epochCacheUpdateLoop.stop();
|
|
154
242
|
}
|
|
155
243
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
244
|
+
/** Register handlers on the p2p client */
|
|
245
|
+
public async registerHandlers() {
|
|
246
|
+
if (!this.hasRegisteredHandlers) {
|
|
247
|
+
this.hasRegisteredHandlers = true;
|
|
248
|
+
this.log.debug(`Registering validator handlers for p2p client`);
|
|
162
249
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
* We reuse the sequencer's block building functionality for re-execution
|
|
167
|
-
*/
|
|
168
|
-
public registerBlockBuilder(blockBuilder: BlockBuilderCallback) {
|
|
169
|
-
this.blockBuilder = blockBuilder;
|
|
170
|
-
}
|
|
250
|
+
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
|
|
251
|
+
this.attestToProposal(block, proposalSender);
|
|
252
|
+
this.p2pClient.registerBlockProposalHandler(handler);
|
|
171
253
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const proposalInfo = {
|
|
175
|
-
slotNumber,
|
|
176
|
-
blockNumber: proposal.payload.header.globalVariables.blockNumber.toNumber(),
|
|
177
|
-
archive: proposal.payload.archive.toString(),
|
|
178
|
-
txCount: proposal.payload.txHashes.length,
|
|
179
|
-
txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
|
|
180
|
-
};
|
|
181
|
-
this.log.verbose(`Received request to attest for slot ${slotNumber}`);
|
|
182
|
-
|
|
183
|
-
// Check that I am in the committee
|
|
184
|
-
if (!(await this.epochCache.isInCommittee(this.keyStore.getAddress()))) {
|
|
185
|
-
this.log.verbose(`Not in the committee, skipping attestation`);
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
254
|
+
const myAddresses = this.getValidatorAddresses();
|
|
255
|
+
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
188
256
|
|
|
189
|
-
|
|
190
|
-
const invalidProposal = await this.blockProposalValidator.validate(proposal);
|
|
191
|
-
if (invalidProposal) {
|
|
192
|
-
this.log.verbose(`Proposal is not valid, skipping attestation`);
|
|
193
|
-
return undefined;
|
|
257
|
+
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
194
258
|
}
|
|
259
|
+
}
|
|
195
260
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
await this.ensureTransactionsAreAvailable(proposal);
|
|
261
|
+
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
|
|
262
|
+
const slotNumber = proposal.slotNumber.toBigInt();
|
|
263
|
+
const proposer = proposal.getSender();
|
|
200
264
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
} catch (error: any) {
|
|
206
|
-
// If the transactions are not available, then we should not attempt to attest
|
|
207
|
-
if (error instanceof TransactionsNotAvailableError) {
|
|
208
|
-
this.log.error(`Transactions not available, skipping attestation`, error, proposalInfo);
|
|
209
|
-
} else {
|
|
210
|
-
// This branch most commonly be hit if the transactions are available, but the re-execution fails
|
|
211
|
-
// Catch all error handler
|
|
212
|
-
this.log.error(`Failed to attest to proposal`, error, proposalInfo);
|
|
213
|
-
}
|
|
265
|
+
// Reject proposals with invalid signatures
|
|
266
|
+
if (!proposer) {
|
|
267
|
+
this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
|
|
214
268
|
return undefined;
|
|
215
269
|
}
|
|
216
270
|
|
|
217
|
-
//
|
|
218
|
-
this.
|
|
271
|
+
// Check that I have any address in current committee before attesting
|
|
272
|
+
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
273
|
+
const partOfCommittee = inCommittee.length > 0;
|
|
219
274
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
275
|
+
const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() };
|
|
276
|
+
this.log.info(`Received proposal for slot ${slotNumber}`, {
|
|
277
|
+
...proposalInfo,
|
|
278
|
+
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
279
|
+
});
|
|
223
280
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
281
|
+
// Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
|
|
282
|
+
// invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
|
|
283
|
+
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals } = this.config;
|
|
284
|
+
const shouldReexecute =
|
|
285
|
+
(slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
|
|
286
|
+
(partOfCommittee && validatorReexecute) ||
|
|
287
|
+
alwaysReexecuteBlockProposals;
|
|
288
|
+
|
|
289
|
+
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
290
|
+
proposal,
|
|
291
|
+
proposalSender,
|
|
292
|
+
!!shouldReexecute,
|
|
293
|
+
);
|
|
230
294
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
295
|
+
if (!validationResult.isValid) {
|
|
296
|
+
this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
297
|
+
|
|
298
|
+
const reason = validationResult.reason || 'unknown';
|
|
299
|
+
// Classify failure reason: bad proposal vs node issue
|
|
300
|
+
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
301
|
+
'invalid_proposal',
|
|
302
|
+
'state_mismatch',
|
|
303
|
+
'failed_txs',
|
|
304
|
+
'in_hash_mismatch',
|
|
305
|
+
'parent_block_wrong_slot',
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
|
|
309
|
+
this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
|
|
310
|
+
} else {
|
|
311
|
+
// Node issues so we can't attest
|
|
312
|
+
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
313
|
+
}
|
|
234
314
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
315
|
+
// Slash invalid block proposals (can happen even when not in committee)
|
|
316
|
+
if (
|
|
317
|
+
validationResult.reason &&
|
|
318
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
319
|
+
slashBroadcastedInvalidBlockPenalty > 0n
|
|
320
|
+
) {
|
|
321
|
+
this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
|
|
322
|
+
this.slashInvalidBlock(proposal);
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
238
325
|
}
|
|
239
326
|
|
|
240
|
-
//
|
|
241
|
-
if (
|
|
242
|
-
|
|
327
|
+
// Check that I have any address in current committee before attesting
|
|
328
|
+
if (!partOfCommittee) {
|
|
329
|
+
this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
|
|
330
|
+
return undefined;
|
|
243
331
|
}
|
|
244
332
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
validateOnly: true,
|
|
249
|
-
});
|
|
250
|
-
stopTimer();
|
|
251
|
-
|
|
252
|
-
this.log.verbose(`Transaction re-execution complete`);
|
|
333
|
+
// Provided all of the above checks pass, we can attest to the proposal
|
|
334
|
+
this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
|
|
335
|
+
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
253
336
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
337
|
+
// If the above function does not throw an error, then we can attest to the proposal
|
|
338
|
+
return this.createBlockAttestationsFromProposal(proposal, inCommittee);
|
|
339
|
+
}
|
|
258
340
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
throw new ReExTimeoutError();
|
|
262
|
-
}
|
|
341
|
+
private slashInvalidBlock(proposal: BlockProposal) {
|
|
342
|
+
const proposer = proposal.getSender();
|
|
263
343
|
|
|
264
|
-
//
|
|
265
|
-
if (!
|
|
266
|
-
|
|
267
|
-
|
|
344
|
+
// Skip if signature is invalid (shouldn't happen since we validate earlier)
|
|
345
|
+
if (!proposer) {
|
|
346
|
+
this.log.warn(`Cannot slash proposal with invalid signature`);
|
|
347
|
+
return;
|
|
268
348
|
}
|
|
269
|
-
}
|
|
270
349
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
* 2. If any transactions are not in the local tx pool, request them from the network
|
|
276
|
-
* 3. If we cannot retrieve them from the network, throw an error
|
|
277
|
-
* @param proposal - The proposal to attest to
|
|
278
|
-
*/
|
|
279
|
-
async ensureTransactionsAreAvailable(proposal: BlockProposal) {
|
|
280
|
-
const txHashes: TxHash[] = proposal.payload.txHashes;
|
|
281
|
-
const transactionStatuses = await Promise.all(txHashes.map(txHash => this.p2pClient.getTxStatus(txHash)));
|
|
282
|
-
|
|
283
|
-
const missingTxs = txHashes.filter((_, index) => !['pending', 'mined'].includes(transactionStatuses[index] ?? ''));
|
|
284
|
-
|
|
285
|
-
if (missingTxs.length === 0) {
|
|
286
|
-
return; // All transactions are available
|
|
350
|
+
// Trim the set if it's too big.
|
|
351
|
+
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
352
|
+
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
353
|
+
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
287
354
|
}
|
|
288
355
|
|
|
289
|
-
this.
|
|
356
|
+
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
290
357
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
358
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
359
|
+
{
|
|
360
|
+
validator: proposer,
|
|
361
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
362
|
+
offenseType: OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL,
|
|
363
|
+
epochOrSlot: proposal.slotNumber.toBigInt(),
|
|
364
|
+
},
|
|
365
|
+
]);
|
|
295
366
|
}
|
|
296
367
|
|
|
297
|
-
async createBlockProposal(
|
|
298
|
-
|
|
368
|
+
async createBlockProposal(
|
|
369
|
+
blockNumber: number,
|
|
370
|
+
header: CheckpointHeader,
|
|
371
|
+
archive: Fr,
|
|
372
|
+
stateReference: StateReference,
|
|
373
|
+
txs: Tx[],
|
|
374
|
+
proposerAddress: EthAddress | undefined,
|
|
375
|
+
options: BlockProposalOptions,
|
|
376
|
+
): Promise<BlockProposal | undefined> {
|
|
377
|
+
if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
|
|
299
378
|
this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
300
379
|
return Promise.resolve(undefined);
|
|
301
380
|
}
|
|
302
381
|
|
|
303
|
-
const newProposal = await this.validationService.createBlockProposal(
|
|
382
|
+
const newProposal = await this.validationService.createBlockProposal(
|
|
383
|
+
header,
|
|
384
|
+
archive,
|
|
385
|
+
stateReference,
|
|
386
|
+
txs,
|
|
387
|
+
proposerAddress,
|
|
388
|
+
{ ...options, broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal },
|
|
389
|
+
);
|
|
304
390
|
this.previousProposal = newProposal;
|
|
305
391
|
return newProposal;
|
|
306
392
|
}
|
|
307
393
|
|
|
308
|
-
broadcastBlockProposal(proposal: BlockProposal): void {
|
|
309
|
-
this.p2pClient.broadcastProposal(proposal);
|
|
394
|
+
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
395
|
+
await this.p2pClient.broadcastProposal(proposal);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async signAttestationsAndSigners(
|
|
399
|
+
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
400
|
+
proposer: EthAddress,
|
|
401
|
+
): Promise<Signature> {
|
|
402
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
|
|
406
|
+
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
407
|
+
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
408
|
+
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
409
|
+
return this.createBlockAttestationsFromProposal(proposal, inCommittee);
|
|
310
410
|
}
|
|
311
411
|
|
|
312
|
-
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
|
|
313
412
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
314
413
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
315
|
-
const slot = proposal.payload.header.
|
|
414
|
+
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
316
415
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
317
416
|
|
|
318
417
|
if (+deadline < this.dateProvider.now()) {
|
|
319
418
|
this.log.error(
|
|
320
419
|
`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`,
|
|
321
420
|
);
|
|
322
|
-
throw new AttestationTimeoutError(required, slot);
|
|
421
|
+
throw new AttestationTimeoutError(0, required, slot);
|
|
323
422
|
}
|
|
324
423
|
|
|
424
|
+
await this.collectOwnAttestations(proposal);
|
|
425
|
+
|
|
325
426
|
const proposalId = proposal.archive.toString();
|
|
326
|
-
const
|
|
427
|
+
const myAddresses = this.getValidatorAddresses();
|
|
327
428
|
|
|
328
429
|
let attestations: BlockAttestation[] = [];
|
|
329
430
|
while (true) {
|
|
330
|
-
|
|
331
|
-
|
|
431
|
+
// Filter out attestations with a mismatching payload. This should NOT happen since we have verified
|
|
432
|
+
// the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
|
|
433
|
+
const collectedAttestations = (await this.p2pClient.getAttestationsForSlot(slot, proposalId)).filter(
|
|
434
|
+
attestation => {
|
|
435
|
+
if (!attestation.payload.equals(proposal.payload)) {
|
|
436
|
+
this.log.warn(
|
|
437
|
+
`Received attestation for slot ${slot} with mismatched payload from ${attestation.getSender()?.toString()}`,
|
|
438
|
+
{ attestationPayload: attestation.payload, proposalPayload: proposal.payload },
|
|
439
|
+
);
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Log new attestations we collected
|
|
447
|
+
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
332
448
|
for (const collected of collectedAttestations) {
|
|
333
|
-
const collectedSender =
|
|
334
|
-
|
|
449
|
+
const collectedSender = collected.getSender();
|
|
450
|
+
// Skip attestations with invalid signatures
|
|
451
|
+
if (!collectedSender) {
|
|
452
|
+
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (
|
|
456
|
+
!myAddresses.some(address => address.equals(collectedSender)) &&
|
|
457
|
+
!oldSenders.some(sender => sender?.equals(collectedSender))
|
|
458
|
+
) {
|
|
335
459
|
this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
|
|
336
460
|
}
|
|
337
461
|
}
|
|
@@ -344,19 +468,43 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
344
468
|
|
|
345
469
|
if (+deadline < this.dateProvider.now()) {
|
|
346
470
|
this.log.error(`Timeout ${deadline.toISOString()} waiting for ${required} attestations for slot ${slot}`);
|
|
347
|
-
throw new AttestationTimeoutError(required, slot);
|
|
471
|
+
throw new AttestationTimeoutError(attestations.length, required, slot);
|
|
348
472
|
}
|
|
349
473
|
|
|
350
|
-
this.log.debug(`Collected ${attestations.length} attestations so far`);
|
|
474
|
+
this.log.debug(`Collected ${attestations.length} of ${required} attestations so far`);
|
|
351
475
|
await sleep(this.config.attestationPollingIntervalMs);
|
|
352
476
|
}
|
|
353
477
|
}
|
|
354
|
-
}
|
|
355
478
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
479
|
+
private async createBlockAttestationsFromProposal(
|
|
480
|
+
proposal: BlockProposal,
|
|
481
|
+
attestors: EthAddress[] = [],
|
|
482
|
+
): Promise<BlockAttestation[]> {
|
|
483
|
+
const attestations = await this.validationService.attestToProposal(proposal, attestors);
|
|
484
|
+
await this.p2pClient.addAttestations(attestations);
|
|
485
|
+
return attestations;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private async handleAuthRequest(peer: PeerId, msg: Buffer): Promise<Buffer> {
|
|
489
|
+
const authRequest = AuthRequest.fromBuffer(msg);
|
|
490
|
+
const statusMessage = await this.p2pClient.handleAuthRequestFromPeer(authRequest, peer).catch(_ => undefined);
|
|
491
|
+
if (statusMessage === undefined) {
|
|
492
|
+
return Buffer.alloc(0);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Find a validator address that is in the set
|
|
496
|
+
const allRegisteredValidators = await this.epochCache.getRegisteredValidators();
|
|
497
|
+
const addressToUse = this.getValidatorAddresses().find(
|
|
498
|
+
address => allRegisteredValidators.find(v => v.equals(address)) !== undefined,
|
|
499
|
+
);
|
|
500
|
+
if (addressToUse === undefined) {
|
|
501
|
+
// We don't have a registered address
|
|
502
|
+
return Buffer.alloc(0);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const payloadToSign = authRequest.getPayloadToSign();
|
|
506
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
|
|
507
|
+
const authResponse = new AuthResponse(statusMessage, signature);
|
|
508
|
+
return authResponse.toBuffer();
|
|
361
509
|
}
|
|
362
510
|
}
|