@aztec-labs/validator-client 6.0.0-nightly.20260829
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +327 -0
- package/dest/checkpoint_builder.d.ts +92 -0
- package/dest/checkpoint_builder.d.ts.map +1 -0
- package/dest/checkpoint_builder.js +272 -0
- package/dest/config.d.ts +17 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +102 -0
- package/dest/duties/validation_service.d.ts +65 -0
- package/dest/duties/validation_service.d.ts.map +1 -0
- package/dest/duties/validation_service.js +128 -0
- package/dest/factory.d.ts +41 -0
- package/dest/factory.d.ts.map +1 -0
- package/dest/factory.js +19 -0
- package/dest/index.d.ts +7 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +6 -0
- package/dest/key_store/ha_key_store.d.ts +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +6 -0
- package/dest/key_store/index.d.ts.map +1 -0
- package/dest/key_store/index.js +5 -0
- package/dest/key_store/interface.d.ts +104 -0
- package/dest/key_store/interface.d.ts.map +1 -0
- package/dest/key_store/interface.js +4 -0
- package/dest/key_store/local_key_store.d.ts +63 -0
- package/dest/key_store/local_key_store.d.ts.map +1 -0
- package/dest/key_store/local_key_store.js +83 -0
- package/dest/key_store/node_keystore_adapter.d.ts +151 -0
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -0
- package/dest/key_store/node_keystore_adapter.js +330 -0
- package/dest/key_store/web3signer_key_store.d.ts +74 -0
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -0
- package/dest/key_store/web3signer_key_store.js +147 -0
- package/dest/metrics.d.ts +31 -0
- package/dest/metrics.d.ts.map +1 -0
- package/dest/metrics.js +101 -0
- package/dest/proposal_handler.d.ts +188 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/proposal_handler.js +1438 -0
- package/dest/streaming_inbox_checks.d.ts +103 -0
- package/dest/streaming_inbox_checks.d.ts.map +1 -0
- package/dest/streaming_inbox_checks.js +112 -0
- package/dest/validator.d.ts +137 -0
- package/dest/validator.d.ts.map +1 -0
- package/dest/validator.js +771 -0
- package/package.json +110 -0
- package/src/checkpoint_builder.ts +449 -0
- package/src/config.ts +130 -0
- package/src/duties/validation_service.ts +224 -0
- package/src/factory.ts +95 -0
- package/src/index.ts +6 -0
- package/src/key_store/ha_key_store.ts +268 -0
- package/src/key_store/index.ts +5 -0
- package/src/key_store/interface.ts +120 -0
- package/src/key_store/local_key_store.ts +104 -0
- package/src/key_store/node_keystore_adapter.ts +397 -0
- package/src/key_store/web3signer_key_store.ts +188 -0
- package/src/metrics.ts +150 -0
- package/src/proposal_handler.ts +1598 -0
- package/src/streaming_inbox_checks.ts +198 -0
- package/src/validator.ts +1125 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { Buffer32 } from '@aztec-labs/foundation/buffer';
|
|
2
|
+
import { normalizeSignature } from '@aztec-labs/foundation/crypto/secp256k1-signer';
|
|
3
|
+
import { EthAddress } from '@aztec-labs/foundation/eth-address';
|
|
4
|
+
import { Signature } from '@aztec-labs/foundation/eth-signature';
|
|
5
|
+
import type { SigningContext } from '@aztec-labs/validator-ha-signer/types';
|
|
6
|
+
import type { TypedDataDefinition } from 'viem';
|
|
7
|
+
|
|
8
|
+
import type { ValidatorKeyStore } from './interface.js';
|
|
9
|
+
|
|
10
|
+
/** Default hard timeout (ms) applied to each Web3Signer HTTP request. */
|
|
11
|
+
const DEFAULT_WEB3SIGNER_REQUEST_TIMEOUT_MS = 30_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Web3Signer Key Store
|
|
15
|
+
*
|
|
16
|
+
* An implementation of the Key store using Web3Signer remote signing service.
|
|
17
|
+
* This implementation uses the Web3Signer JSON-RPC API for secp256k1 signatures.
|
|
18
|
+
*/
|
|
19
|
+
export class Web3SignerKeyStore implements ValidatorKeyStore {
|
|
20
|
+
private readonly requestTimeoutMs: number;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private addresses: EthAddress[],
|
|
24
|
+
private baseUrl: string,
|
|
25
|
+
requestTimeoutMs: number = DEFAULT_WEB3SIGNER_REQUEST_TIMEOUT_MS,
|
|
26
|
+
) {
|
|
27
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the address of a signer by index
|
|
32
|
+
*
|
|
33
|
+
* @param index - The index of the signer
|
|
34
|
+
* @returns the address
|
|
35
|
+
*/
|
|
36
|
+
public getAddress(index: number): EthAddress {
|
|
37
|
+
if (index >= this.addresses.length) {
|
|
38
|
+
throw new Error(`Index ${index} is out of bounds.`);
|
|
39
|
+
}
|
|
40
|
+
return this.addresses[index];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get all addresses
|
|
45
|
+
*
|
|
46
|
+
* @returns all addresses
|
|
47
|
+
*/
|
|
48
|
+
public getAddresses(): EthAddress[] {
|
|
49
|
+
return this.addresses;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sign EIP-712 typed data with all keystore addresses
|
|
54
|
+
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
55
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
56
|
+
* @return signatures
|
|
57
|
+
*/
|
|
58
|
+
public signTypedData(typedData: TypedDataDefinition, _context: SigningContext): Promise<Signature[]> {
|
|
59
|
+
return Promise.all(this.addresses.map(address => this.makeJsonRpcSignTypedDataRequest(address, typedData)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sign EIP-712 typed data with a specific address
|
|
64
|
+
* @param address - The address of the signer to use
|
|
65
|
+
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
66
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
67
|
+
* @returns signature for the specified address
|
|
68
|
+
* @throws Error if the address is not found in the keystore or signing fails
|
|
69
|
+
*/
|
|
70
|
+
public async signTypedDataWithAddress(
|
|
71
|
+
address: EthAddress,
|
|
72
|
+
typedData: TypedDataDefinition,
|
|
73
|
+
_context: SigningContext,
|
|
74
|
+
): Promise<Signature> {
|
|
75
|
+
if (!this.addresses.some(addr => addr.equals(address))) {
|
|
76
|
+
throw new Error(`Address ${address.toString()} not found in keystore`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return await this.makeJsonRpcSignTypedDataRequest(address, typedData);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sign a message with all keystore addresses using EIP-191 prefix
|
|
84
|
+
*
|
|
85
|
+
* @param message - The message to sign
|
|
86
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
87
|
+
* @return signatures
|
|
88
|
+
*/
|
|
89
|
+
public signMessage(message: Buffer32, _context: SigningContext): Promise<Signature[]> {
|
|
90
|
+
return Promise.all(this.addresses.map(address => this.makeJsonRpcSignRequest(address, message)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Sign a message with a specific address using EIP-191 prefix
|
|
95
|
+
* @param address - The address of the signer to use
|
|
96
|
+
* @param message - The message to sign
|
|
97
|
+
* @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
|
|
98
|
+
* @returns signature for the specified address
|
|
99
|
+
* @throws Error if the address is not found in the keystore or signing fails
|
|
100
|
+
*/
|
|
101
|
+
public async signMessageWithAddress(
|
|
102
|
+
address: EthAddress,
|
|
103
|
+
message: Buffer32,
|
|
104
|
+
_context: SigningContext,
|
|
105
|
+
): Promise<Signature> {
|
|
106
|
+
if (!this.addresses.some(addr => addr.equals(address))) {
|
|
107
|
+
throw new Error(`Address ${address.toString()} not found in keystore`);
|
|
108
|
+
}
|
|
109
|
+
return await this.makeJsonRpcSignRequest(address, message);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Make a JSON-RPC sign request to Web3Signer using eth_sign
|
|
114
|
+
* @param address - The Ethereum address to sign with
|
|
115
|
+
* @param data - The data to sign
|
|
116
|
+
* @returns The signature
|
|
117
|
+
*/
|
|
118
|
+
private makeJsonRpcSignRequest(address: EthAddress, data: Buffer32): Promise<Signature> {
|
|
119
|
+
// eth_sign automatically applies Ethereum message prefixing to the raw data.
|
|
120
|
+
return this.sendSignRequest({
|
|
121
|
+
jsonrpc: '2.0',
|
|
122
|
+
method: 'eth_sign',
|
|
123
|
+
params: [address.toString(), data.toString()],
|
|
124
|
+
id: 1,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private makeJsonRpcSignTypedDataRequest(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
|
|
129
|
+
return this.sendSignRequest({
|
|
130
|
+
jsonrpc: '2.0',
|
|
131
|
+
method: 'eth_signTypedData',
|
|
132
|
+
params: [address.toString(), JSON.stringify(typedData)],
|
|
133
|
+
id: 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Send a JSON-RPC request to Web3Signer under a hard request timeout and parse the signature.
|
|
139
|
+
* A timed-out or aborted request is surfaced as a clear timeout error rather than hanging, so a
|
|
140
|
+
* slow or unreachable signer cannot stall an HA signing operation past its own timeout budget.
|
|
141
|
+
*/
|
|
142
|
+
private async sendSignRequest(body: object): Promise<Signature> {
|
|
143
|
+
let response: Response;
|
|
144
|
+
try {
|
|
145
|
+
response = await fetch(this.baseUrl, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (
|
|
155
|
+
(err instanceof Error || err instanceof DOMException) &&
|
|
156
|
+
(err.name === 'TimeoutError' || err.name === 'AbortError')
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(`Web3Signer request timed out after ${this.requestTimeoutMs}ms`);
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const errorText = await response.text();
|
|
165
|
+
throw new Error(`Web3Signer request failed: ${response.status} ${response.statusText} - ${errorText}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = await response.json();
|
|
169
|
+
|
|
170
|
+
// Handle JSON-RPC response format
|
|
171
|
+
if (result.error) {
|
|
172
|
+
throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!result.result) {
|
|
176
|
+
throw new Error('Invalid response from Web3Signer: no result found');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let signatureHex = result.result;
|
|
180
|
+
|
|
181
|
+
// Ensure the signature has the 0x prefix
|
|
182
|
+
if (!signatureHex.startsWith('0x')) {
|
|
183
|
+
signatureHex = '0x' + signatureHex;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return normalizeSignature(Signature.fromString(signatureHex as `0x${string}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { EpochNumber } from '@aztec-labs/foundation/branded-types';
|
|
2
|
+
import type { EthAddress } from '@aztec-labs/foundation/eth-address';
|
|
3
|
+
import type { BlockProposal } from '@aztec-labs/stdlib/p2p';
|
|
4
|
+
import {
|
|
5
|
+
Attributes,
|
|
6
|
+
type Gauge,
|
|
7
|
+
type Histogram,
|
|
8
|
+
Metrics,
|
|
9
|
+
type TelemetryClient,
|
|
10
|
+
type UpDownCounter,
|
|
11
|
+
createUpDownCounterWithDefault,
|
|
12
|
+
} from '@aztec-labs/telemetry-client';
|
|
13
|
+
|
|
14
|
+
import type { BlockProposalValidationFailureReason } from './proposal_handler.js';
|
|
15
|
+
|
|
16
|
+
export class ValidatorMetrics {
|
|
17
|
+
private failedReexecutionCounter: UpDownCounter;
|
|
18
|
+
private successfulAttestationsCount: UpDownCounter;
|
|
19
|
+
private failedAttestationsBadProposalCount: UpDownCounter;
|
|
20
|
+
private failedAttestationsNodeIssueCount: UpDownCounter;
|
|
21
|
+
private currentEpoch: Gauge;
|
|
22
|
+
private attestedEpochCount: UpDownCounter;
|
|
23
|
+
|
|
24
|
+
private reexMana: Histogram;
|
|
25
|
+
private reexTx: Histogram;
|
|
26
|
+
private reexDuration: Gauge;
|
|
27
|
+
private checkpointProposalToPipelinedStateDuration: Histogram;
|
|
28
|
+
private checkpointProposalReceiveOffsetFromNextSlotBoundary: Histogram;
|
|
29
|
+
|
|
30
|
+
constructor(telemetryClient: TelemetryClient) {
|
|
31
|
+
const meter = telemetryClient.getMeter('Validator');
|
|
32
|
+
|
|
33
|
+
this.failedReexecutionCounter = createUpDownCounterWithDefault(meter, Metrics.VALIDATOR_FAILED_REEXECUTION_COUNT, {
|
|
34
|
+
[Attributes.STATUS]: ['failed'],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.successfulAttestationsCount = createUpDownCounterWithDefault(
|
|
38
|
+
meter,
|
|
39
|
+
Metrics.VALIDATOR_ATTESTATION_SUCCESS_COUNT,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
this.failedAttestationsBadProposalCount = createUpDownCounterWithDefault(
|
|
43
|
+
meter,
|
|
44
|
+
Metrics.VALIDATOR_ATTESTATION_FAILED_BAD_PROPOSAL_COUNT,
|
|
45
|
+
{
|
|
46
|
+
[Attributes.ERROR_TYPE]: [
|
|
47
|
+
'invalid_proposal',
|
|
48
|
+
'state_mismatch',
|
|
49
|
+
'failed_txs',
|
|
50
|
+
'parent_block_wrong_slot',
|
|
51
|
+
'duplicate_txs',
|
|
52
|
+
'invalid_embedded_txs',
|
|
53
|
+
],
|
|
54
|
+
[Attributes.IS_COMMITTEE_MEMBER]: [true, false],
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
this.failedAttestationsNodeIssueCount = createUpDownCounterWithDefault(
|
|
59
|
+
meter,
|
|
60
|
+
Metrics.VALIDATOR_ATTESTATION_FAILED_NODE_ISSUE_COUNT,
|
|
61
|
+
{
|
|
62
|
+
[Attributes.ERROR_TYPE]: [
|
|
63
|
+
'parent_block_not_found',
|
|
64
|
+
'global_variables_mismatch',
|
|
65
|
+
'block_number_already_exists',
|
|
66
|
+
'txs_not_available',
|
|
67
|
+
'timeout',
|
|
68
|
+
'unknown_error',
|
|
69
|
+
],
|
|
70
|
+
[Attributes.IS_COMMITTEE_MEMBER]: [true, false],
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
this.currentEpoch = meter.createGauge(Metrics.VALIDATOR_CURRENT_EPOCH);
|
|
75
|
+
|
|
76
|
+
this.attestedEpochCount = createUpDownCounterWithDefault(meter, Metrics.VALIDATOR_ATTESTED_EPOCH_COUNT);
|
|
77
|
+
|
|
78
|
+
this.reexMana = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_MANA);
|
|
79
|
+
|
|
80
|
+
this.reexTx = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_TX_COUNT);
|
|
81
|
+
|
|
82
|
+
this.reexDuration = meter.createGauge(Metrics.VALIDATOR_RE_EXECUTION_TIME);
|
|
83
|
+
this.checkpointProposalToPipelinedStateDuration = meter.createHistogram(
|
|
84
|
+
Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_TO_PIPELINED_STATE_DURATION,
|
|
85
|
+
);
|
|
86
|
+
this.checkpointProposalReceiveOffsetFromNextSlotBoundary = meter.createHistogram(
|
|
87
|
+
Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_RECEIVE_OFFSET_FROM_NEXT_SLOT_BOUNDARY,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public recordReex(time: number, txs: number, mManaTotal: number) {
|
|
92
|
+
this.reexDuration.record(Math.ceil(time));
|
|
93
|
+
this.reexTx.record(txs);
|
|
94
|
+
this.reexMana.record(mManaTotal);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public recordCheckpointProposalToPipelinedStateDuration(durationMs: number) {
|
|
98
|
+
this.checkpointProposalToPipelinedStateDuration.record(Math.ceil(durationMs));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public recordCheckpointProposalReceiveOffsetFromNextSlotBoundary(offsetMs: number) {
|
|
102
|
+
this.checkpointProposalReceiveOffsetFromNextSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
|
|
103
|
+
[Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public recordFailedReexecution(proposal: BlockProposal) {
|
|
108
|
+
const proposer = proposal.getSender();
|
|
109
|
+
this.failedReexecutionCounter.add(1, {
|
|
110
|
+
[Attributes.STATUS]: 'failed',
|
|
111
|
+
[Attributes.BLOCK_PROPOSER]: proposer?.toString() ?? 'unknown',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public incSuccessfulAttestations(num: number) {
|
|
116
|
+
this.successfulAttestationsCount.add(num);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public incFailedAttestationsBadProposal(
|
|
120
|
+
num: number,
|
|
121
|
+
reason: BlockProposalValidationFailureReason,
|
|
122
|
+
inCommittee: boolean,
|
|
123
|
+
) {
|
|
124
|
+
this.failedAttestationsBadProposalCount.add(num, {
|
|
125
|
+
[Attributes.ERROR_TYPE]: reason,
|
|
126
|
+
[Attributes.IS_COMMITTEE_MEMBER]: inCommittee,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public incFailedAttestationsNodeIssue(
|
|
131
|
+
num: number,
|
|
132
|
+
reason: BlockProposalValidationFailureReason,
|
|
133
|
+
inCommittee: boolean,
|
|
134
|
+
) {
|
|
135
|
+
this.failedAttestationsNodeIssueCount.add(num, {
|
|
136
|
+
[Attributes.ERROR_TYPE]: reason,
|
|
137
|
+
[Attributes.IS_COMMITTEE_MEMBER]: inCommittee,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Update the gauge tracking the current epoch number (proxy for total epochs elapsed). */
|
|
142
|
+
public setCurrentEpoch(epoch: EpochNumber) {
|
|
143
|
+
this.currentEpoch.record(Number(epoch));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Increment the count of epochs in which the given attester submitted at least one attestation. */
|
|
147
|
+
public incAttestedEpochCount(attester: EthAddress) {
|
|
148
|
+
this.attestedEpochCount.add(1, { [Attributes.ATTESTER_ADDRESS]: attester.toString() });
|
|
149
|
+
}
|
|
150
|
+
}
|