@aztec/validator-client 1.2.0 → 2.0.0-nightly.20250813
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/config.d.ts +6 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -0
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +1 -1
- package/dest/factory.d.ts +3 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +3 -2
- package/dest/key_store/index.d.ts +1 -0
- 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 +3 -2
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +8 -7
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +12 -8
- 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 +153 -0
- package/dest/validator.d.ts +9 -6
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +96 -55
- package/package.json +10 -10
- package/src/config.ts +18 -1
- package/src/duties/validation_service.ts +2 -1
- package/src/factory.ts +9 -5
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +4 -2
- package/src/key_store/local_key_store.ts +13 -9
- package/src/key_store/web3signer_key_store.ts +195 -0
- package/src/validator.ts +114 -63
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Buffer32 } from '@aztec/foundation/buffer';
|
|
2
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
4
|
+
|
|
5
|
+
import type { TypedDataDefinition } from 'viem';
|
|
6
|
+
|
|
7
|
+
import type { ValidatorKeyStore } from './interface.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Web3Signer Key Store
|
|
11
|
+
*
|
|
12
|
+
* An implementation of the Key store using Web3Signer remote signing service.
|
|
13
|
+
* This implementation uses the Web3Signer JSON-RPC API for secp256k1 signatures.
|
|
14
|
+
*/
|
|
15
|
+
export class Web3SignerKeyStore implements ValidatorKeyStore {
|
|
16
|
+
constructor(
|
|
17
|
+
private addresses: EthAddress[],
|
|
18
|
+
private baseUrl: string,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the address of a signer by index
|
|
23
|
+
*
|
|
24
|
+
* @param index - The index of the signer
|
|
25
|
+
* @returns the address
|
|
26
|
+
*/
|
|
27
|
+
public getAddress(index: number): EthAddress {
|
|
28
|
+
if (index >= this.addresses.length) {
|
|
29
|
+
throw new Error(`Index ${index} is out of bounds.`);
|
|
30
|
+
}
|
|
31
|
+
return this.addresses[index];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get all addresses
|
|
36
|
+
*
|
|
37
|
+
* @returns all addresses
|
|
38
|
+
*/
|
|
39
|
+
public getAddresses(): EthAddress[] {
|
|
40
|
+
return this.addresses;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sign EIP-712 typed data with all keystore addresses
|
|
45
|
+
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
46
|
+
* @return signatures
|
|
47
|
+
*/
|
|
48
|
+
public async signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
|
|
49
|
+
const signatures = await Promise.all(
|
|
50
|
+
this.addresses.map(address => this.makeJsonRpcSignTypedDataRequest(address, typedData)),
|
|
51
|
+
);
|
|
52
|
+
return signatures;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sign EIP-712 typed data with a specific address
|
|
57
|
+
* @param address - The address of the signer to use
|
|
58
|
+
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
59
|
+
* @returns signature for the specified address
|
|
60
|
+
* @throws Error if the address is not found in the keystore or signing fails
|
|
61
|
+
*/
|
|
62
|
+
public async signTypedDataWithAddress(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
|
|
63
|
+
if (!this.addresses.some(addr => addr.equals(address))) {
|
|
64
|
+
throw new Error(`Address ${address.toString()} not found in keystore`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return await this.makeJsonRpcSignTypedDataRequest(address, typedData);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sign a message with all keystore addresses using EIP-191 prefix
|
|
72
|
+
*
|
|
73
|
+
* @param message - The message to sign
|
|
74
|
+
* @return signatures
|
|
75
|
+
*/
|
|
76
|
+
public async signMessage(message: Buffer32): Promise<Signature[]> {
|
|
77
|
+
const signatures = await Promise.all(this.addresses.map(address => this.makeJsonRpcSignRequest(address, message)));
|
|
78
|
+
return signatures;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sign a message with a specific address using EIP-191 prefix
|
|
83
|
+
* @param address - The address of the signer to use
|
|
84
|
+
* @param message - The message to sign
|
|
85
|
+
* @returns signature for the specified address
|
|
86
|
+
* @throws Error if the address is not found in the keystore or signing fails
|
|
87
|
+
*/
|
|
88
|
+
public async signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
|
|
89
|
+
if (!this.addresses.some(addr => addr.equals(address))) {
|
|
90
|
+
throw new Error(`Address ${address.toString()} not found in keystore`);
|
|
91
|
+
}
|
|
92
|
+
return await this.makeJsonRpcSignRequest(address, message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Make a JSON-RPC sign request to Web3Signer using eth_sign
|
|
97
|
+
* @param address - The Ethereum address to sign with
|
|
98
|
+
* @param data - The data to sign
|
|
99
|
+
* @returns The signature
|
|
100
|
+
*/
|
|
101
|
+
private async makeJsonRpcSignRequest(address: EthAddress, data: Buffer32): Promise<Signature> {
|
|
102
|
+
const url = this.baseUrl;
|
|
103
|
+
|
|
104
|
+
// Use JSON-RPC eth_sign method which automatically applies Ethereum message prefixing
|
|
105
|
+
const body = {
|
|
106
|
+
jsonrpc: '2.0',
|
|
107
|
+
method: 'eth_sign',
|
|
108
|
+
params: [
|
|
109
|
+
address.toString(), // Ethereum address as identifier
|
|
110
|
+
data.toString(), // Raw data to sign (eth_sign will apply Ethereum message prefix)
|
|
111
|
+
],
|
|
112
|
+
id: 1,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const response = await fetch(url, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify(body),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorText = await response.text();
|
|
125
|
+
throw new Error(`Web3Signer request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = await response.json();
|
|
129
|
+
|
|
130
|
+
// Handle JSON-RPC response format
|
|
131
|
+
if (result.error) {
|
|
132
|
+
throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!result.result) {
|
|
136
|
+
throw new Error('Invalid response from Web3Signer: no result found');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let signatureHex = result.result;
|
|
140
|
+
|
|
141
|
+
// Ensure the signature has the 0x prefix
|
|
142
|
+
if (!signatureHex.startsWith('0x')) {
|
|
143
|
+
signatureHex = '0x' + signatureHex;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Parse the signature from the hex string
|
|
147
|
+
return Signature.fromString(signatureHex as `0x${string}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async makeJsonRpcSignTypedDataRequest(
|
|
151
|
+
address: EthAddress,
|
|
152
|
+
typedData: TypedDataDefinition,
|
|
153
|
+
): Promise<Signature> {
|
|
154
|
+
const url = this.baseUrl;
|
|
155
|
+
|
|
156
|
+
const body = {
|
|
157
|
+
jsonrpc: '2.0',
|
|
158
|
+
method: 'eth_signTypedData',
|
|
159
|
+
params: [address.toString(), JSON.stringify(typedData)],
|
|
160
|
+
id: 1,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const response = await fetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const errorText = await response.text();
|
|
173
|
+
throw new Error(`Web3Signer request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await response.json();
|
|
177
|
+
|
|
178
|
+
if (result.error) {
|
|
179
|
+
throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!result.result) {
|
|
183
|
+
throw new Error('Invalid response from Web3Signer: no result found');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let signatureHex = result.result;
|
|
187
|
+
|
|
188
|
+
// Ensure the signature has the 0x prefix
|
|
189
|
+
if (!signatureHex.startsWith('0x')) {
|
|
190
|
+
signatureHex = '0x' + signatureHex;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return Signature.fromString(signatureHex as `0x${string}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/validator.ts
CHANGED
|
@@ -9,11 +9,11 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
9
9
|
import { sleep } from '@aztec/foundation/sleep';
|
|
10
10
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
11
11
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
12
|
-
import {
|
|
12
|
+
import { AuthRequest, AuthResponse, ReqRespSubProtocol, TxProvider } from '@aztec/p2p';
|
|
13
13
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
14
14
|
import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
|
|
15
|
+
import { Offense } from '@aztec/slasher';
|
|
15
16
|
import {
|
|
16
|
-
Offense,
|
|
17
17
|
type SlasherConfig,
|
|
18
18
|
WANT_TO_SLASH_EVENT,
|
|
19
19
|
type WantToSlashArgs,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from '@aztec/slasher/config';
|
|
23
23
|
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
24
24
|
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
25
|
-
import type { IFullNodeBlockBuilder,
|
|
25
|
+
import type { IFullNodeBlockBuilder, SequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
26
26
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
27
27
|
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
28
28
|
import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
|
|
@@ -37,11 +37,13 @@ import {
|
|
|
37
37
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
38
38
|
|
|
39
39
|
import { EventEmitter } from 'events';
|
|
40
|
+
import type { TypedDataDefinition } from 'viem';
|
|
40
41
|
|
|
41
42
|
import type { ValidatorClientConfig } from './config.js';
|
|
42
43
|
import { ValidationService } from './duties/validation_service.js';
|
|
43
44
|
import type { ValidatorKeyStore } from './key_store/interface.js';
|
|
44
45
|
import { LocalKeyStore } from './key_store/local_key_store.js';
|
|
46
|
+
import { Web3SignerKeyStore } from './key_store/web3signer_key_store.js';
|
|
45
47
|
import { ValidatorMetrics } from './metrics.js';
|
|
46
48
|
|
|
47
49
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
@@ -80,20 +82,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
80
82
|
private previousProposal?: BlockProposal;
|
|
81
83
|
|
|
82
84
|
private myAddresses: EthAddress[];
|
|
83
|
-
private
|
|
85
|
+
private lastEpochForCommitteeUpdateLoop: bigint | undefined;
|
|
84
86
|
private epochCacheUpdateLoop: RunningPromise;
|
|
85
87
|
|
|
86
88
|
private blockProposalValidator: BlockProposalValidator;
|
|
87
|
-
private txCollector: ITxCollector;
|
|
88
|
-
private proposersOfInvalidBlocks: Set<EthAddress> = new Set();
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
91
|
+
|
|
92
|
+
protected constructor(
|
|
91
93
|
private blockBuilder: IFullNodeBlockBuilder,
|
|
92
94
|
private keyStore: ValidatorKeyStore,
|
|
93
95
|
private epochCache: EpochCache,
|
|
94
96
|
private p2pClient: P2P,
|
|
95
97
|
private blockSource: L2BlockSource,
|
|
96
98
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
99
|
+
private txProvider: TxProvider,
|
|
97
100
|
private config: ValidatorClientConfig &
|
|
98
101
|
Pick<SequencerConfig, 'txPublicSetupAllowList'> &
|
|
99
102
|
Pick<SlasherConfig, 'slashInvalidBlockEnabled' | 'slashInvalidBlockPenalty' | 'slashInvalidBlockMaxPenalty'>,
|
|
@@ -109,8 +112,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
109
112
|
|
|
110
113
|
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
111
114
|
|
|
112
|
-
this.txCollector = new TxCollector(p2pClient, this.log);
|
|
113
|
-
|
|
114
115
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
115
116
|
this.myAddresses = this.keyStore.getAddresses();
|
|
116
117
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
@@ -120,12 +121,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
120
121
|
|
|
121
122
|
private async handleEpochCommitteeUpdate() {
|
|
122
123
|
try {
|
|
123
|
-
const { committee, epoch } = await this.epochCache.getCommittee('
|
|
124
|
+
const { committee, epoch } = await this.epochCache.getCommittee('next');
|
|
124
125
|
if (!committee) {
|
|
125
126
|
this.log.trace(`No committee found for slot`);
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
|
-
if (epoch !== this.
|
|
129
|
+
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
129
130
|
const me = this.myAddresses;
|
|
130
131
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
131
132
|
const inCommittee = me.filter(a => committeeSet.has(a.toString()));
|
|
@@ -138,7 +139,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
138
139
|
`Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
|
|
139
140
|
);
|
|
140
141
|
}
|
|
141
|
-
this.
|
|
142
|
+
this.lastEpochForCommitteeUpdateLoop = epoch;
|
|
142
143
|
}
|
|
143
144
|
} catch (err) {
|
|
144
145
|
this.log.error(`Error updating epoch committee`, err);
|
|
@@ -153,27 +154,40 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
153
154
|
p2pClient: P2P,
|
|
154
155
|
blockSource: L2BlockSource,
|
|
155
156
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
157
|
+
txProvider: TxProvider,
|
|
156
158
|
dateProvider: DateProvider = new DateProvider(),
|
|
157
159
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
158
160
|
) {
|
|
159
|
-
|
|
160
|
-
throw new InvalidValidatorPrivateKeyError();
|
|
161
|
-
}
|
|
161
|
+
let keyStore: ValidatorKeyStore;
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
if (config.web3SignerUrl) {
|
|
164
|
+
const addresses = config.web3SignerAddresses;
|
|
165
|
+
if (!addresses?.length) {
|
|
166
|
+
throw new Error('web3SignerAddresses is required when web3SignerUrl is provided');
|
|
167
|
+
}
|
|
168
|
+
keyStore = new Web3SignerKeyStore(addresses, config.web3SignerUrl);
|
|
169
|
+
} else {
|
|
170
|
+
const privateKeys = config.validatorPrivateKeys?.getValue().map(validatePrivateKey);
|
|
171
|
+
if (!privateKeys?.length) {
|
|
172
|
+
throw new InvalidValidatorPrivateKeyError();
|
|
173
|
+
}
|
|
174
|
+
keyStore = new LocalKeyStore(privateKeys);
|
|
175
|
+
}
|
|
165
176
|
|
|
166
177
|
const validator = new ValidatorClient(
|
|
167
178
|
blockBuilder,
|
|
168
|
-
|
|
179
|
+
keyStore,
|
|
169
180
|
epochCache,
|
|
170
181
|
p2pClient,
|
|
171
182
|
blockSource,
|
|
172
183
|
l1ToL2MessageSource,
|
|
184
|
+
txProvider,
|
|
173
185
|
config,
|
|
174
186
|
dateProvider,
|
|
175
187
|
telemetry,
|
|
176
188
|
);
|
|
189
|
+
|
|
190
|
+
// TODO(PhilWindle): This seems like it could/should be done inside start()
|
|
177
191
|
validator.registerBlockProposalHandler();
|
|
178
192
|
return validator;
|
|
179
193
|
}
|
|
@@ -182,8 +196,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
182
196
|
return this.keyStore.getAddresses();
|
|
183
197
|
}
|
|
184
198
|
|
|
185
|
-
public signWithAddress(addr: EthAddress, msg:
|
|
186
|
-
return this.keyStore.
|
|
199
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
200
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
public configureSlashing(
|
|
@@ -203,15 +217,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
203
217
|
|
|
204
218
|
const myAddresses = this.keyStore.getAddresses();
|
|
205
219
|
|
|
206
|
-
const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
|
|
220
|
+
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
207
221
|
if (inCommittee.length > 0) {
|
|
208
222
|
this.log.info(
|
|
209
|
-
`Started validator with addresses in current validator committee: ${inCommittee
|
|
223
|
+
`Started validator with addresses in current validator committee: ${inCommittee
|
|
224
|
+
.map(a => a.toString())
|
|
225
|
+
.join(', ')}`,
|
|
210
226
|
);
|
|
211
227
|
} else {
|
|
212
228
|
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
213
229
|
}
|
|
214
230
|
this.epochCacheUpdateLoop.start();
|
|
231
|
+
|
|
232
|
+
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
233
|
+
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
234
|
+
|
|
215
235
|
return Promise.resolve();
|
|
216
236
|
}
|
|
217
237
|
|
|
@@ -220,36 +240,48 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
public registerBlockProposalHandler() {
|
|
223
|
-
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
|
|
224
|
-
|
|
225
|
-
};
|
|
243
|
+
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
|
|
244
|
+
this.attestToProposal(block, proposalSender);
|
|
226
245
|
this.p2pClient.registerBlockProposalHandler(handler);
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
|
|
230
|
-
const slotNumber = proposal.slotNumber.
|
|
249
|
+
const slotNumber = proposal.slotNumber.toBigInt();
|
|
231
250
|
const blockNumber = proposal.blockNumber;
|
|
232
251
|
const proposer = proposal.getSender();
|
|
233
252
|
|
|
234
253
|
// Check that I have any address in current committee before attesting
|
|
235
|
-
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
254
|
+
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.keyStore.getAddresses());
|
|
236
255
|
const partOfCommittee = inCommittee.length > 0;
|
|
237
256
|
|
|
238
257
|
const proposalInfo = {
|
|
239
|
-
|
|
240
|
-
blockNumber,
|
|
258
|
+
...proposal.toBlockInfo(),
|
|
241
259
|
proposer: proposer.toString(),
|
|
242
|
-
archive: proposal.payload.archive.toString(),
|
|
243
|
-
txCount: proposal.payload.txHashes.length,
|
|
244
|
-
txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
|
|
245
260
|
};
|
|
246
|
-
|
|
261
|
+
|
|
262
|
+
this.log.info(`Received proposal for slot ${slotNumber}`, {
|
|
263
|
+
...proposalInfo,
|
|
264
|
+
txHashes: proposal.txHashes.map(txHash => txHash.toString()),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Collect txs from the proposal. Note that we do this before checking if we have an address in the
|
|
268
|
+
// current committee, since we want to collect txs anyway to facilitate propagation.
|
|
269
|
+
const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, {
|
|
270
|
+
pinnedPeer: proposalSender,
|
|
271
|
+
deadline: this.getReexecutionDeadline(proposal, this.blockBuilder.getConfig()),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Check that I have any address in current committee before attesting
|
|
275
|
+
if (!partOfCommittee) {
|
|
276
|
+
this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
247
279
|
|
|
248
280
|
// Check that the proposal is from the current proposer, or the next proposer.
|
|
249
281
|
// Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
|
|
250
282
|
const invalidProposal = await this.blockProposalValidator.validate(proposal);
|
|
251
283
|
if (invalidProposal) {
|
|
252
|
-
this.log.warn(`Proposal is not valid, skipping attestation
|
|
284
|
+
this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
|
|
253
285
|
if (partOfCommittee) {
|
|
254
286
|
this.metrics.incFailedAttestations(1, 'invalid_proposal');
|
|
255
287
|
}
|
|
@@ -284,14 +316,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
284
316
|
);
|
|
285
317
|
|
|
286
318
|
if (parentBlock === undefined) {
|
|
287
|
-
this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation
|
|
288
|
-
|
|
319
|
+
this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
|
|
289
320
|
if (partOfCommittee) {
|
|
290
321
|
this.metrics.incFailedAttestations(1, 'parent_block_not_found');
|
|
291
322
|
}
|
|
292
|
-
|
|
293
323
|
return undefined;
|
|
294
324
|
}
|
|
325
|
+
|
|
295
326
|
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
296
327
|
this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
|
|
297
328
|
proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
|
|
@@ -305,22 +336,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
305
336
|
}
|
|
306
337
|
}
|
|
307
338
|
|
|
308
|
-
// Collect txs from the proposal
|
|
309
|
-
const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
|
|
310
|
-
|
|
311
|
-
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
312
|
-
if (missing && missing.length > 0) {
|
|
313
|
-
this.log.warn(`Missing ${missing.length}/${proposal.payload.txHashes.length} txs to attest to proposal`, {
|
|
314
|
-
...proposalInfo,
|
|
315
|
-
missing,
|
|
316
|
-
});
|
|
317
|
-
if (partOfCommittee) {
|
|
318
|
-
this.metrics.incFailedAttestations(1, 'tx_not_available');
|
|
319
|
-
}
|
|
320
|
-
return undefined;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
339
|
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
340
|
+
// Q: Same as above, should this be part of p2p validation?
|
|
324
341
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
|
|
325
342
|
const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
326
343
|
const proposalInHash = proposal.payload.header.contentCommitment.inHash;
|
|
@@ -336,8 +353,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
353
|
return undefined;
|
|
337
354
|
}
|
|
338
355
|
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
357
|
+
if (missingTxs.length > 0) {
|
|
358
|
+
this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, { ...proposalInfo, missingTxs });
|
|
359
|
+
if (partOfCommittee) {
|
|
360
|
+
this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
|
|
361
|
+
}
|
|
341
362
|
return undefined;
|
|
342
363
|
}
|
|
343
364
|
|
|
@@ -380,11 +401,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
380
401
|
* @param proposal - The proposal to re-execute
|
|
381
402
|
*/
|
|
382
403
|
async reExecuteTransactions(proposal: BlockProposal, txs: Tx[], l1ToL2Messages: Fr[]): Promise<void> {
|
|
383
|
-
const { header
|
|
404
|
+
const { header } = proposal.payload;
|
|
405
|
+
const { txHashes } = proposal;
|
|
384
406
|
|
|
385
407
|
// If we do not have all of the transactions, then we should fail
|
|
386
408
|
if (txs.length !== txHashes.length) {
|
|
387
|
-
const foundTxHashes =
|
|
409
|
+
const foundTxHashes = txs.map(tx => tx.getTxHash());
|
|
388
410
|
const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
|
|
389
411
|
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
390
412
|
}
|
|
@@ -440,13 +462,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
440
462
|
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
441
463
|
}
|
|
442
464
|
|
|
443
|
-
this.proposersOfInvalidBlocks.add(proposer);
|
|
465
|
+
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
444
466
|
|
|
445
467
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
446
468
|
{
|
|
447
469
|
validator: proposer,
|
|
448
470
|
amount: this.config.slashInvalidBlockPenalty,
|
|
449
|
-
offense: Offense.
|
|
471
|
+
offense: Offense.BROADCASTED_INVALID_BLOCK_PROPOSAL,
|
|
450
472
|
},
|
|
451
473
|
]);
|
|
452
474
|
}
|
|
@@ -466,7 +488,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
466
488
|
public shouldSlash(args: WantToSlashArgs): Promise<boolean> {
|
|
467
489
|
// note we don't check the offence here: we know this person is bad and we're willing to slash up to the max penalty.
|
|
468
490
|
return Promise.resolve(
|
|
469
|
-
args.amount <= this.config.slashInvalidBlockMaxPenalty &&
|
|
491
|
+
args.amount <= this.config.slashInvalidBlockMaxPenalty &&
|
|
492
|
+
this.proposersOfInvalidBlocks.has(args.validator.toString()),
|
|
470
493
|
);
|
|
471
494
|
}
|
|
472
495
|
|
|
@@ -501,6 +524,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
501
524
|
await this.p2pClient.broadcastProposal(proposal);
|
|
502
525
|
}
|
|
503
526
|
|
|
527
|
+
async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
|
|
528
|
+
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
529
|
+
const inCommittee = await this.epochCache.filterInCommittee(slot, this.keyStore.getAddresses());
|
|
530
|
+
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
531
|
+
return this.doAttestToProposal(proposal, inCommittee);
|
|
532
|
+
}
|
|
533
|
+
|
|
504
534
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
505
535
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
506
536
|
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
@@ -513,11 +543,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
513
543
|
throw new AttestationTimeoutError(0, required, slot);
|
|
514
544
|
}
|
|
515
545
|
|
|
516
|
-
|
|
517
|
-
// adds attestations for all of my addresses locally
|
|
518
|
-
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
519
|
-
await this.doAttestToProposal(proposal, inCommittee);
|
|
546
|
+
await this.collectOwnAttestations(proposal);
|
|
520
547
|
|
|
548
|
+
const proposalId = proposal.archive.toString();
|
|
521
549
|
const myAddresses = this.keyStore.getAddresses();
|
|
522
550
|
|
|
523
551
|
let attestations: BlockAttestation[] = [];
|
|
@@ -555,6 +583,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
555
583
|
await this.p2pClient.addAttestations(attestations);
|
|
556
584
|
return attestations;
|
|
557
585
|
}
|
|
586
|
+
|
|
587
|
+
private async handleAuthRequest(peer: PeerId, msg: Buffer): Promise<Buffer> {
|
|
588
|
+
const authRequest = AuthRequest.fromBuffer(msg);
|
|
589
|
+
const statusMessage = await this.p2pClient.handleAuthRequestFromPeer(authRequest, peer).catch(_ => undefined);
|
|
590
|
+
if (statusMessage === undefined) {
|
|
591
|
+
return Buffer.alloc(0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Find a validator address that is in the set
|
|
595
|
+
const allRegisteredValidators = await this.epochCache.getRegisteredValidators();
|
|
596
|
+
const addressToUse = this.getValidatorAddresses().find(
|
|
597
|
+
address => allRegisteredValidators.find(v => v.equals(address)) !== undefined,
|
|
598
|
+
);
|
|
599
|
+
if (addressToUse === undefined) {
|
|
600
|
+
// We don't have a registered address
|
|
601
|
+
return Buffer.alloc(0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const payloadToSign = authRequest.getPayloadToSign();
|
|
605
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
|
|
606
|
+
const authResponse = new AuthResponse(statusMessage, signature);
|
|
607
|
+
return authResponse.toBuffer();
|
|
608
|
+
}
|
|
558
609
|
}
|
|
559
610
|
|
|
560
611
|
function validatePrivateKey(privateKey: string): Buffer32 {
|