@aztec/validator-client 1.2.1 → 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 +2 -2
- 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 +11 -8
- 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 +8 -3
- 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 +115 -68
|
@@ -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,12 @@ 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
|
-
|
|
17
|
+
type SlasherConfig,
|
|
17
18
|
WANT_TO_SLASH_EVENT,
|
|
18
19
|
type WantToSlashArgs,
|
|
19
20
|
type Watcher,
|
|
@@ -21,12 +22,7 @@ import {
|
|
|
21
22
|
} from '@aztec/slasher/config';
|
|
22
23
|
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
23
24
|
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
|
-
import type {
|
|
25
|
-
IFullNodeBlockBuilder,
|
|
26
|
-
ITxCollector,
|
|
27
|
-
SequencerConfig,
|
|
28
|
-
SlasherConfig,
|
|
29
|
-
} from '@aztec/stdlib/interfaces/server';
|
|
25
|
+
import type { IFullNodeBlockBuilder, SequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
30
26
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
31
27
|
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
32
28
|
import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
|
|
@@ -41,11 +37,13 @@ import {
|
|
|
41
37
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
42
38
|
|
|
43
39
|
import { EventEmitter } from 'events';
|
|
40
|
+
import type { TypedDataDefinition } from 'viem';
|
|
44
41
|
|
|
45
42
|
import type { ValidatorClientConfig } from './config.js';
|
|
46
43
|
import { ValidationService } from './duties/validation_service.js';
|
|
47
44
|
import type { ValidatorKeyStore } from './key_store/interface.js';
|
|
48
45
|
import { LocalKeyStore } from './key_store/local_key_store.js';
|
|
46
|
+
import { Web3SignerKeyStore } from './key_store/web3signer_key_store.js';
|
|
49
47
|
import { ValidatorMetrics } from './metrics.js';
|
|
50
48
|
|
|
51
49
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
@@ -84,20 +82,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
84
82
|
private previousProposal?: BlockProposal;
|
|
85
83
|
|
|
86
84
|
private myAddresses: EthAddress[];
|
|
87
|
-
private
|
|
85
|
+
private lastEpochForCommitteeUpdateLoop: bigint | undefined;
|
|
88
86
|
private epochCacheUpdateLoop: RunningPromise;
|
|
89
87
|
|
|
90
88
|
private blockProposalValidator: BlockProposalValidator;
|
|
91
|
-
private txCollector: ITxCollector;
|
|
92
|
-
private proposersOfInvalidBlocks: Set<EthAddress> = new Set();
|
|
93
89
|
|
|
94
|
-
|
|
90
|
+
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
91
|
+
|
|
92
|
+
protected constructor(
|
|
95
93
|
private blockBuilder: IFullNodeBlockBuilder,
|
|
96
94
|
private keyStore: ValidatorKeyStore,
|
|
97
95
|
private epochCache: EpochCache,
|
|
98
96
|
private p2pClient: P2P,
|
|
99
97
|
private blockSource: L2BlockSource,
|
|
100
98
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
99
|
+
private txProvider: TxProvider,
|
|
101
100
|
private config: ValidatorClientConfig &
|
|
102
101
|
Pick<SequencerConfig, 'txPublicSetupAllowList'> &
|
|
103
102
|
Pick<SlasherConfig, 'slashInvalidBlockEnabled' | 'slashInvalidBlockPenalty' | 'slashInvalidBlockMaxPenalty'>,
|
|
@@ -113,8 +112,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
113
112
|
|
|
114
113
|
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
115
114
|
|
|
116
|
-
this.txCollector = new TxCollector(p2pClient, this.log);
|
|
117
|
-
|
|
118
115
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
119
116
|
this.myAddresses = this.keyStore.getAddresses();
|
|
120
117
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
@@ -124,12 +121,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
124
121
|
|
|
125
122
|
private async handleEpochCommitteeUpdate() {
|
|
126
123
|
try {
|
|
127
|
-
const { committee, epoch } = await this.epochCache.getCommittee('
|
|
124
|
+
const { committee, epoch } = await this.epochCache.getCommittee('next');
|
|
128
125
|
if (!committee) {
|
|
129
126
|
this.log.trace(`No committee found for slot`);
|
|
130
127
|
return;
|
|
131
128
|
}
|
|
132
|
-
if (epoch !== this.
|
|
129
|
+
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
133
130
|
const me = this.myAddresses;
|
|
134
131
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
135
132
|
const inCommittee = me.filter(a => committeeSet.has(a.toString()));
|
|
@@ -142,7 +139,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
142
139
|
`Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
|
|
143
140
|
);
|
|
144
141
|
}
|
|
145
|
-
this.
|
|
142
|
+
this.lastEpochForCommitteeUpdateLoop = epoch;
|
|
146
143
|
}
|
|
147
144
|
} catch (err) {
|
|
148
145
|
this.log.error(`Error updating epoch committee`, err);
|
|
@@ -157,27 +154,40 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
157
154
|
p2pClient: P2P,
|
|
158
155
|
blockSource: L2BlockSource,
|
|
159
156
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
157
|
+
txProvider: TxProvider,
|
|
160
158
|
dateProvider: DateProvider = new DateProvider(),
|
|
161
159
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
162
160
|
) {
|
|
163
|
-
|
|
164
|
-
throw new InvalidValidatorPrivateKeyError();
|
|
165
|
-
}
|
|
161
|
+
let keyStore: ValidatorKeyStore;
|
|
166
162
|
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
176
|
|
|
170
177
|
const validator = new ValidatorClient(
|
|
171
178
|
blockBuilder,
|
|
172
|
-
|
|
179
|
+
keyStore,
|
|
173
180
|
epochCache,
|
|
174
181
|
p2pClient,
|
|
175
182
|
blockSource,
|
|
176
183
|
l1ToL2MessageSource,
|
|
184
|
+
txProvider,
|
|
177
185
|
config,
|
|
178
186
|
dateProvider,
|
|
179
187
|
telemetry,
|
|
180
188
|
);
|
|
189
|
+
|
|
190
|
+
// TODO(PhilWindle): This seems like it could/should be done inside start()
|
|
181
191
|
validator.registerBlockProposalHandler();
|
|
182
192
|
return validator;
|
|
183
193
|
}
|
|
@@ -186,8 +196,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
186
196
|
return this.keyStore.getAddresses();
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
public signWithAddress(addr: EthAddress, msg:
|
|
190
|
-
return this.keyStore.
|
|
199
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
200
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
public configureSlashing(
|
|
@@ -207,15 +217,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
207
217
|
|
|
208
218
|
const myAddresses = this.keyStore.getAddresses();
|
|
209
219
|
|
|
210
|
-
const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
|
|
220
|
+
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
211
221
|
if (inCommittee.length > 0) {
|
|
212
222
|
this.log.info(
|
|
213
|
-
`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(', ')}`,
|
|
214
226
|
);
|
|
215
227
|
} else {
|
|
216
228
|
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
217
229
|
}
|
|
218
230
|
this.epochCacheUpdateLoop.start();
|
|
231
|
+
|
|
232
|
+
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
233
|
+
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
234
|
+
|
|
219
235
|
return Promise.resolve();
|
|
220
236
|
}
|
|
221
237
|
|
|
@@ -224,36 +240,48 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
224
240
|
}
|
|
225
241
|
|
|
226
242
|
public registerBlockProposalHandler() {
|
|
227
|
-
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
|
|
228
|
-
|
|
229
|
-
};
|
|
243
|
+
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
|
|
244
|
+
this.attestToProposal(block, proposalSender);
|
|
230
245
|
this.p2pClient.registerBlockProposalHandler(handler);
|
|
231
246
|
}
|
|
232
247
|
|
|
233
248
|
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
|
|
234
|
-
const slotNumber = proposal.slotNumber.
|
|
249
|
+
const slotNumber = proposal.slotNumber.toBigInt();
|
|
235
250
|
const blockNumber = proposal.blockNumber;
|
|
236
251
|
const proposer = proposal.getSender();
|
|
237
252
|
|
|
238
253
|
// Check that I have any address in current committee before attesting
|
|
239
|
-
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
254
|
+
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.keyStore.getAddresses());
|
|
240
255
|
const partOfCommittee = inCommittee.length > 0;
|
|
241
256
|
|
|
242
257
|
const proposalInfo = {
|
|
243
|
-
|
|
244
|
-
blockNumber,
|
|
258
|
+
...proposal.toBlockInfo(),
|
|
245
259
|
proposer: proposer.toString(),
|
|
246
|
-
archive: proposal.payload.archive.toString(),
|
|
247
|
-
txCount: proposal.payload.txHashes.length,
|
|
248
|
-
txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
|
|
249
260
|
};
|
|
250
|
-
|
|
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
|
+
}
|
|
251
279
|
|
|
252
280
|
// Check that the proposal is from the current proposer, or the next proposer.
|
|
253
281
|
// Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
|
|
254
282
|
const invalidProposal = await this.blockProposalValidator.validate(proposal);
|
|
255
283
|
if (invalidProposal) {
|
|
256
|
-
this.log.warn(`Proposal is not valid, skipping attestation
|
|
284
|
+
this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
|
|
257
285
|
if (partOfCommittee) {
|
|
258
286
|
this.metrics.incFailedAttestations(1, 'invalid_proposal');
|
|
259
287
|
}
|
|
@@ -288,14 +316,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
288
316
|
);
|
|
289
317
|
|
|
290
318
|
if (parentBlock === undefined) {
|
|
291
|
-
this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation
|
|
292
|
-
|
|
319
|
+
this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
|
|
293
320
|
if (partOfCommittee) {
|
|
294
321
|
this.metrics.incFailedAttestations(1, 'parent_block_not_found');
|
|
295
322
|
}
|
|
296
|
-
|
|
297
323
|
return undefined;
|
|
298
324
|
}
|
|
325
|
+
|
|
299
326
|
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
300
327
|
this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
|
|
301
328
|
proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
|
|
@@ -309,22 +336,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
336
|
}
|
|
310
337
|
}
|
|
311
338
|
|
|
312
|
-
// Collect txs from the proposal
|
|
313
|
-
const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
|
|
314
|
-
|
|
315
|
-
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
316
|
-
if (missing && missing.length > 0) {
|
|
317
|
-
this.log.warn(`Missing ${missing.length}/${proposal.payload.txHashes.length} txs to attest to proposal`, {
|
|
318
|
-
...proposalInfo,
|
|
319
|
-
missing,
|
|
320
|
-
});
|
|
321
|
-
if (partOfCommittee) {
|
|
322
|
-
this.metrics.incFailedAttestations(1, 'tx_not_available');
|
|
323
|
-
}
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
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?
|
|
328
341
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
|
|
329
342
|
const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
330
343
|
const proposalInHash = proposal.payload.header.contentCommitment.inHash;
|
|
@@ -340,8 +353,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
353
|
return undefined;
|
|
341
354
|
}
|
|
342
355
|
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
}
|
|
345
362
|
return undefined;
|
|
346
363
|
}
|
|
347
364
|
|
|
@@ -384,11 +401,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
384
401
|
* @param proposal - The proposal to re-execute
|
|
385
402
|
*/
|
|
386
403
|
async reExecuteTransactions(proposal: BlockProposal, txs: Tx[], l1ToL2Messages: Fr[]): Promise<void> {
|
|
387
|
-
const { header
|
|
404
|
+
const { header } = proposal.payload;
|
|
405
|
+
const { txHashes } = proposal;
|
|
388
406
|
|
|
389
407
|
// If we do not have all of the transactions, then we should fail
|
|
390
408
|
if (txs.length !== txHashes.length) {
|
|
391
|
-
const foundTxHashes =
|
|
409
|
+
const foundTxHashes = txs.map(tx => tx.getTxHash());
|
|
392
410
|
const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
|
|
393
411
|
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
394
412
|
}
|
|
@@ -444,13 +462,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
444
462
|
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
445
463
|
}
|
|
446
464
|
|
|
447
|
-
this.proposersOfInvalidBlocks.add(proposer);
|
|
465
|
+
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
448
466
|
|
|
449
467
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
450
468
|
{
|
|
451
469
|
validator: proposer,
|
|
452
470
|
amount: this.config.slashInvalidBlockPenalty,
|
|
453
|
-
offense: Offense.
|
|
471
|
+
offense: Offense.BROADCASTED_INVALID_BLOCK_PROPOSAL,
|
|
454
472
|
},
|
|
455
473
|
]);
|
|
456
474
|
}
|
|
@@ -470,7 +488,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
470
488
|
public shouldSlash(args: WantToSlashArgs): Promise<boolean> {
|
|
471
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.
|
|
472
490
|
return Promise.resolve(
|
|
473
|
-
args.amount <= this.config.slashInvalidBlockMaxPenalty &&
|
|
491
|
+
args.amount <= this.config.slashInvalidBlockMaxPenalty &&
|
|
492
|
+
this.proposersOfInvalidBlocks.has(args.validator.toString()),
|
|
474
493
|
);
|
|
475
494
|
}
|
|
476
495
|
|
|
@@ -505,6 +524,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
505
524
|
await this.p2pClient.broadcastProposal(proposal);
|
|
506
525
|
}
|
|
507
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
|
+
|
|
508
534
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
509
535
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
510
536
|
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
@@ -517,11 +543,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
517
543
|
throw new AttestationTimeoutError(0, required, slot);
|
|
518
544
|
}
|
|
519
545
|
|
|
520
|
-
|
|
521
|
-
// adds attestations for all of my addresses locally
|
|
522
|
-
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
523
|
-
await this.doAttestToProposal(proposal, inCommittee);
|
|
546
|
+
await this.collectOwnAttestations(proposal);
|
|
524
547
|
|
|
548
|
+
const proposalId = proposal.archive.toString();
|
|
525
549
|
const myAddresses = this.keyStore.getAddresses();
|
|
526
550
|
|
|
527
551
|
let attestations: BlockAttestation[] = [];
|
|
@@ -559,6 +583,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
559
583
|
await this.p2pClient.addAttestations(attestations);
|
|
560
584
|
return attestations;
|
|
561
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
|
+
}
|
|
562
609
|
}
|
|
563
610
|
|
|
564
611
|
function validatePrivateKey(privateKey: string): Buffer32 {
|