@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.
Files changed (62) hide show
  1. package/README.md +327 -0
  2. package/dest/checkpoint_builder.d.ts +92 -0
  3. package/dest/checkpoint_builder.d.ts.map +1 -0
  4. package/dest/checkpoint_builder.js +272 -0
  5. package/dest/config.d.ts +17 -0
  6. package/dest/config.d.ts.map +1 -0
  7. package/dest/config.js +102 -0
  8. package/dest/duties/validation_service.d.ts +65 -0
  9. package/dest/duties/validation_service.d.ts.map +1 -0
  10. package/dest/duties/validation_service.js +128 -0
  11. package/dest/factory.d.ts +41 -0
  12. package/dest/factory.d.ts.map +1 -0
  13. package/dest/factory.js +19 -0
  14. package/dest/index.d.ts +7 -0
  15. package/dest/index.d.ts.map +1 -0
  16. package/dest/index.js +6 -0
  17. package/dest/key_store/ha_key_store.d.ts +99 -0
  18. package/dest/key_store/ha_key_store.d.ts.map +1 -0
  19. package/dest/key_store/ha_key_store.js +208 -0
  20. package/dest/key_store/index.d.ts +6 -0
  21. package/dest/key_store/index.d.ts.map +1 -0
  22. package/dest/key_store/index.js +5 -0
  23. package/dest/key_store/interface.d.ts +104 -0
  24. package/dest/key_store/interface.d.ts.map +1 -0
  25. package/dest/key_store/interface.js +4 -0
  26. package/dest/key_store/local_key_store.d.ts +63 -0
  27. package/dest/key_store/local_key_store.d.ts.map +1 -0
  28. package/dest/key_store/local_key_store.js +83 -0
  29. package/dest/key_store/node_keystore_adapter.d.ts +151 -0
  30. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -0
  31. package/dest/key_store/node_keystore_adapter.js +330 -0
  32. package/dest/key_store/web3signer_key_store.d.ts +74 -0
  33. package/dest/key_store/web3signer_key_store.d.ts.map +1 -0
  34. package/dest/key_store/web3signer_key_store.js +147 -0
  35. package/dest/metrics.d.ts +31 -0
  36. package/dest/metrics.d.ts.map +1 -0
  37. package/dest/metrics.js +101 -0
  38. package/dest/proposal_handler.d.ts +188 -0
  39. package/dest/proposal_handler.d.ts.map +1 -0
  40. package/dest/proposal_handler.js +1438 -0
  41. package/dest/streaming_inbox_checks.d.ts +103 -0
  42. package/dest/streaming_inbox_checks.d.ts.map +1 -0
  43. package/dest/streaming_inbox_checks.js +112 -0
  44. package/dest/validator.d.ts +137 -0
  45. package/dest/validator.d.ts.map +1 -0
  46. package/dest/validator.js +771 -0
  47. package/package.json +110 -0
  48. package/src/checkpoint_builder.ts +449 -0
  49. package/src/config.ts +130 -0
  50. package/src/duties/validation_service.ts +224 -0
  51. package/src/factory.ts +95 -0
  52. package/src/index.ts +6 -0
  53. package/src/key_store/ha_key_store.ts +268 -0
  54. package/src/key_store/index.ts +5 -0
  55. package/src/key_store/interface.ts +120 -0
  56. package/src/key_store/local_key_store.ts +104 -0
  57. package/src/key_store/node_keystore_adapter.ts +397 -0
  58. package/src/key_store/web3signer_key_store.ts +188 -0
  59. package/src/metrics.ts +150 -0
  60. package/src/proposal_handler.ts +1598 -0
  61. package/src/streaming_inbox_checks.ts +198 -0
  62. 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
+ }