@aztec/validator-client 0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2

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.js ADDED
@@ -0,0 +1,31 @@
1
+ import { NULL_KEY } from '@aztec/ethereum';
2
+ import { booleanConfigHelper, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
3
+ export const validatorClientConfigMappings = {
4
+ validatorPrivateKey: {
5
+ env: 'VALIDATOR_PRIVATE_KEY',
6
+ parseEnv: (val)=>val ? `0x${val.replace('0x', '')}` : NULL_KEY,
7
+ description: 'The private key of the validator participating in attestation duties'
8
+ },
9
+ disableValidator: {
10
+ env: 'VALIDATOR_DISABLED',
11
+ description: 'Do not run the validator',
12
+ ...booleanConfigHelper()
13
+ },
14
+ attestationPollingIntervalMs: {
15
+ env: 'VALIDATOR_ATTESTATIONS_POLLING_INTERVAL_MS',
16
+ description: 'Interval between polling for new attestations',
17
+ ...numberConfigHelper(200)
18
+ },
19
+ validatorReexecute: {
20
+ env: 'VALIDATOR_REEXECUTE',
21
+ description: 'Re-execute transactions before attesting',
22
+ ...booleanConfigHelper(true)
23
+ }
24
+ };
25
+ /**
26
+ * Returns the prover configuration from the environment variables.
27
+ * Note: If an environment variable is not set, the default value is used.
28
+ * @returns The validator configuration.
29
+ */ export function getProverEnvVars() {
30
+ return getConfigFromMappings(validatorClientConfigMappings);
31
+ }
@@ -0,0 +1,35 @@
1
+ import { BlockAttestation, BlockProposal, ConsensusPayload, SignatureDomainSeparator } from '@aztec/circuit-types';
2
+ import { Buffer32 } from '@aztec/foundation/buffer';
3
+ import { keccak256 } from '@aztec/foundation/crypto';
4
+ export class ValidationService {
5
+ keyStore;
6
+ constructor(keyStore){
7
+ this.keyStore = keyStore;
8
+ }
9
+ /**
10
+ * Create a block proposal with the given header, archive, and transactions
11
+ *
12
+ * @param header - The block header
13
+ * @param archive - The archive of the current block
14
+ * @param txs - TxHash[] ordered list of transactions
15
+ *
16
+ * @returns A block proposal signing the above information (not the current implementation!!!)
17
+ */ createBlockProposal(header, archive, txs) {
18
+ const payloadSigner = (payload)=>this.keyStore.signMessage(payload);
19
+ return BlockProposal.createProposalFromSigner(new ConsensusPayload(header, archive, txs), payloadSigner);
20
+ }
21
+ /**
22
+ * Attest to the given block proposal constructed by the current sequencer
23
+ *
24
+ * NOTE: This is just a blind signing.
25
+ * We assume that the proposal is valid and DA guarantees have been checked previously.
26
+ *
27
+ * @param proposal - The proposal to attest to
28
+ * @returns attestation
29
+ */ async attestToProposal(proposal) {
30
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct
31
+ const buf = Buffer32.fromBuffer(keccak256(await proposal.payload.getPayloadToSign(SignatureDomainSeparator.blockAttestation)));
32
+ const sig = await this.keyStore.signMessage(buf);
33
+ return new BlockAttestation(proposal.payload, sig);
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export * from './validator.error.js';
@@ -0,0 +1,45 @@
1
+ export class ValidatorError extends Error {
2
+ constructor(message){
3
+ super(`Validator Error: ${message}`);
4
+ }
5
+ }
6
+ export class InvalidValidatorPrivateKeyError extends ValidatorError {
7
+ constructor(){
8
+ super('Invalid validator private key provided');
9
+ }
10
+ }
11
+ export class AttestationTimeoutError extends ValidatorError {
12
+ constructor(numberOfRequiredAttestations, slot){
13
+ super(`Timeout waiting for ${numberOfRequiredAttestations} attestations for slot, ${slot}`);
14
+ }
15
+ }
16
+ export class TransactionsNotAvailableError extends ValidatorError {
17
+ constructor(txHashes){
18
+ super(`Transactions not available: ${txHashes.join(', ')}`);
19
+ }
20
+ }
21
+ export class FailedToReExecuteTransactionsError extends ValidatorError {
22
+ constructor(txHashes){
23
+ super(`Failed to re-execute transactions: ${txHashes.join(', ')}`);
24
+ }
25
+ }
26
+ export class ReExStateMismatchError extends ValidatorError {
27
+ constructor(){
28
+ super('Re-execution state mismatch');
29
+ }
30
+ }
31
+ export class ReExFailedTxsError extends ValidatorError {
32
+ constructor(numFailedTxs){
33
+ super(`Re-execution failed to process ${numFailedTxs} txs`);
34
+ }
35
+ }
36
+ export class ReExTimeoutError extends ValidatorError {
37
+ constructor(){
38
+ super('Re-execution timed out or failed to process all txs in the proposal');
39
+ }
40
+ }
41
+ export class BlockBuilderNotProvidedError extends ValidatorError {
42
+ constructor(){
43
+ super('Block builder not provided');
44
+ }
45
+ }
@@ -0,0 +1,11 @@
1
+ import { generatePrivateKey } from 'viem/accounts';
2
+ import { ValidatorClient } from './validator.js';
3
+ export function createValidatorClient(config, deps) {
4
+ if (config.disableValidator) {
5
+ return undefined;
6
+ }
7
+ if (config.validatorPrivateKey === undefined || config.validatorPrivateKey === '') {
8
+ config.validatorPrivateKey = generatePrivateKey();
9
+ }
10
+ return ValidatorClient.new(config, deps.epochCache, deps.p2pClient, deps.dateProvider, deps.telemetry);
11
+ }
package/dest/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './config.js';
2
+ export * from './validator.js';
3
+ export * from './factory.js';
@@ -0,0 +1,2 @@
1
+ export * from './interface.js';
2
+ export * from './local_key_store.js';
@@ -0,0 +1,4 @@
1
+ /** Key Store
2
+ *
3
+ * A keystore interface that can be replaced with a local keystore / remote signer service
4
+ */ export { };
@@ -0,0 +1,32 @@
1
+ import { Secp256k1Signer } from '@aztec/foundation/crypto';
2
+ /**
3
+ * Local Key Store
4
+ *
5
+ * An implementation of the Key store using an in memory private key.
6
+ */ export class LocalKeyStore {
7
+ signer;
8
+ constructor(privateKey){
9
+ this.signer = new Secp256k1Signer(privateKey);
10
+ }
11
+ /**
12
+ * Get the address of the signer
13
+ *
14
+ * @returns the address
15
+ */ getAddress() {
16
+ return this.signer.address;
17
+ }
18
+ /**
19
+ * Sign a message with the keystore private key
20
+ *
21
+ * @param messageBuffer - The message buffer to sign
22
+ * @return signature
23
+ */ sign(digest) {
24
+ const signature = this.signer.sign(digest);
25
+ return Promise.resolve(signature);
26
+ }
27
+ signMessage(message) {
28
+ // Sign message adds eth sign prefix and hashes before signing
29
+ const signature = this.signer.signMessage(message);
30
+ return Promise.resolve(signature);
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ import { Attributes, Metrics, ValueType } from '@aztec/telemetry-client';
2
+ export class ValidatorMetrics {
3
+ reExecutionTime;
4
+ failedReexecutionCounter;
5
+ constructor(telemetryClient){
6
+ const meter = telemetryClient.getMeter('Validator');
7
+ this.failedReexecutionCounter = meter.createUpDownCounter(Metrics.VALIDATOR_FAILED_REEXECUTION_COUNT, {
8
+ description: 'The number of failed re-executions',
9
+ unit: 'count',
10
+ valueType: ValueType.INT
11
+ });
12
+ this.reExecutionTime = meter.createGauge(Metrics.VALIDATOR_RE_EXECUTION_TIME, {
13
+ description: 'The time taken to re-execute a transaction',
14
+ unit: 'ms',
15
+ valueType: ValueType.DOUBLE
16
+ });
17
+ }
18
+ reExecutionTimer() {
19
+ const start = performance.now();
20
+ return ()=>{
21
+ const end = performance.now();
22
+ this.recordReExecutionTime(end - start);
23
+ };
24
+ }
25
+ recordReExecutionTime(time) {
26
+ this.reExecutionTime.record(time);
27
+ }
28
+ async recordFailedReexecution(proposal) {
29
+ this.failedReexecutionCounter.add(1, {
30
+ [Attributes.STATUS]: 'failed',
31
+ [Attributes.BLOCK_NUMBER]: proposal.payload.header.globalVariables.blockNumber.toString(),
32
+ [Attributes.BLOCK_PROPOSER]: (await proposal.getSender())?.toString()
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,246 @@
1
+ import { Buffer32 } from '@aztec/foundation/buffer';
2
+ import { createLogger } from '@aztec/foundation/log';
3
+ import { RunningPromise } from '@aztec/foundation/running-promise';
4
+ import { sleep } from '@aztec/foundation/sleep';
5
+ import { DateProvider } from '@aztec/foundation/timer';
6
+ import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
7
+ import { WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
8
+ import { ValidationService } from './duties/validation_service.js';
9
+ import { AttestationTimeoutError, BlockBuilderNotProvidedError, InvalidValidatorPrivateKeyError, ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from './errors/validator.error.js';
10
+ import { LocalKeyStore } from './key_store/local_key_store.js';
11
+ import { ValidatorMetrics } from './metrics.js';
12
+ /**
13
+ * Validator Client
14
+ */ export class ValidatorClient extends WithTracer {
15
+ keyStore;
16
+ epochCache;
17
+ p2pClient;
18
+ config;
19
+ dateProvider;
20
+ log;
21
+ validationService;
22
+ metrics;
23
+ // Used to check if we are sending the same proposal twice
24
+ previousProposal;
25
+ // Callback registered to: sequencer.buildBlock
26
+ blockBuilder;
27
+ epochCacheUpdateLoop;
28
+ blockProposalValidator;
29
+ constructor(keyStore, epochCache, p2pClient, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
30
+ // Instantiate tracer
31
+ super(telemetry, 'Validator'), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.config = config, this.dateProvider = dateProvider, this.log = log, this.blockBuilder = undefined;
32
+ this.metrics = new ValidatorMetrics(telemetry);
33
+ this.validationService = new ValidationService(keyStore);
34
+ this.blockProposalValidator = new BlockProposalValidator(epochCache);
35
+ // Refresh epoch cache every second to trigger commiteeChanged event
36
+ this.epochCacheUpdateLoop = new RunningPromise(()=>this.epochCache.getCommittee().then(()=>{}).catch((err)=>log.error('Error updating validator committee', err)), log, 1000);
37
+ // Listen to commiteeChanged event to alert operator when their validator has entered the committee
38
+ this.epochCache.on('committeeChanged', (newCommittee, epochNumber)=>{
39
+ const me = this.keyStore.getAddress();
40
+ if (newCommittee.some((addr)=>addr.equals(me))) {
41
+ this.log.info(`Validator ${me.toString()} is on the validator committee for epoch ${epochNumber}`);
42
+ } else {
43
+ this.log.verbose(`Validator ${me.toString()} not on the validator committee for epoch ${epochNumber}`);
44
+ }
45
+ });
46
+ this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
47
+ }
48
+ static new(config, epochCache, p2pClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
49
+ if (!config.validatorPrivateKey) {
50
+ throw new InvalidValidatorPrivateKeyError();
51
+ }
52
+ const privateKey = validatePrivateKey(config.validatorPrivateKey);
53
+ const localKeyStore = new LocalKeyStore(privateKey);
54
+ const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, config, dateProvider, telemetry);
55
+ validator.registerBlockProposalHandler();
56
+ return validator;
57
+ }
58
+ async start() {
59
+ // Sync the committee from the smart contract
60
+ // https://github.com/AztecProtocol/aztec-packages/issues/7962
61
+ const me = this.keyStore.getAddress();
62
+ const inCommittee = await this.epochCache.isInCommittee(me);
63
+ if (inCommittee) {
64
+ this.log.info(`Started validator with address ${me.toString()} in current validator committee`);
65
+ } else {
66
+ this.log.info(`Started validator with address ${me.toString()}`);
67
+ }
68
+ this.epochCacheUpdateLoop.start();
69
+ return Promise.resolve();
70
+ }
71
+ async stop() {
72
+ await this.epochCacheUpdateLoop.stop();
73
+ }
74
+ registerBlockProposalHandler() {
75
+ const handler = (block)=>{
76
+ return this.attestToProposal(block);
77
+ };
78
+ this.p2pClient.registerBlockProposalHandler(handler);
79
+ }
80
+ /**
81
+ * Register a callback function for building a block
82
+ *
83
+ * We reuse the sequencer's block building functionality for re-execution
84
+ */ registerBlockBuilder(blockBuilder) {
85
+ this.blockBuilder = blockBuilder;
86
+ }
87
+ async attestToProposal(proposal) {
88
+ const slotNumber = proposal.slotNumber.toNumber();
89
+ const proposalInfo = {
90
+ slotNumber,
91
+ blockNumber: proposal.payload.header.globalVariables.blockNumber.toNumber(),
92
+ archive: proposal.payload.archive.toString(),
93
+ txCount: proposal.payload.txHashes.length,
94
+ txHashes: proposal.payload.txHashes.map((txHash)=>txHash.toString())
95
+ };
96
+ this.log.verbose(`Received request to attest for slot ${slotNumber}`);
97
+ // Check that I am in the committee
98
+ if (!await this.epochCache.isInCommittee(this.keyStore.getAddress())) {
99
+ this.log.verbose(`Not in the committee, skipping attestation`);
100
+ return undefined;
101
+ }
102
+ // Check that the proposal is from the current proposer, or the next proposer.
103
+ const invalidProposal = await this.blockProposalValidator.validate(proposal);
104
+ if (invalidProposal) {
105
+ this.log.verbose(`Proposal is not valid, skipping attestation`);
106
+ return undefined;
107
+ }
108
+ // Check that all of the tranasctions in the proposal are available in the tx pool before attesting
109
+ this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
110
+ try {
111
+ await this.ensureTransactionsAreAvailable(proposal);
112
+ if (this.config.validatorReexecute) {
113
+ this.log.verbose(`Re-executing transactions in the proposal before attesting`);
114
+ await this.reExecuteTransactions(proposal);
115
+ }
116
+ } catch (error) {
117
+ // If the transactions are not available, then we should not attempt to attest
118
+ if (error instanceof TransactionsNotAvailableError) {
119
+ this.log.error(`Transactions not available, skipping attestation`, error, proposalInfo);
120
+ } else {
121
+ // This branch most commonly be hit if the transactions are available, but the re-execution fails
122
+ // Catch all error handler
123
+ this.log.error(`Failed to attest to proposal`, error, proposalInfo);
124
+ }
125
+ return undefined;
126
+ }
127
+ // Provided all of the above checks pass, we can attest to the proposal
128
+ this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
129
+ // If the above function does not throw an error, then we can attest to the proposal
130
+ return this.validationService.attestToProposal(proposal);
131
+ }
132
+ /**
133
+ * Re-execute the transactions in the proposal and check that the state updates match the header state
134
+ * @param proposal - The proposal to re-execute
135
+ */ async reExecuteTransactions(proposal) {
136
+ const { header, txHashes } = proposal.payload;
137
+ const txs = (await Promise.all(txHashes.map((tx)=>this.p2pClient.getTxByHash(tx)))).filter((tx)=>tx !== undefined);
138
+ // If we cannot request all of the transactions, then we should fail
139
+ if (txs.length !== txHashes.length) {
140
+ throw new TransactionsNotAvailableError(txHashes);
141
+ }
142
+ // Assertion: This check will fail if re-execution is not enabled
143
+ if (this.blockBuilder === undefined) {
144
+ throw new BlockBuilderNotProvidedError();
145
+ }
146
+ // Use the sequencer's block building logic to re-execute the transactions
147
+ const stopTimer = this.metrics.reExecutionTimer();
148
+ const { block, numFailedTxs } = await this.blockBuilder(txs, header.globalVariables, {
149
+ validateOnly: true
150
+ });
151
+ stopTimer();
152
+ this.log.verbose(`Transaction re-execution complete`);
153
+ if (numFailedTxs > 0) {
154
+ await this.metrics.recordFailedReexecution(proposal);
155
+ throw new ReExFailedTxsError(numFailedTxs);
156
+ }
157
+ if (block.body.txEffects.length !== txHashes.length) {
158
+ await this.metrics.recordFailedReexecution(proposal);
159
+ throw new ReExTimeoutError();
160
+ }
161
+ // This function will throw an error if state updates do not match
162
+ if (!block.archive.root.equals(proposal.archive)) {
163
+ await this.metrics.recordFailedReexecution(proposal);
164
+ throw new ReExStateMismatchError();
165
+ }
166
+ }
167
+ /**
168
+ * Ensure that all of the transactions in the proposal are available in the tx pool before attesting
169
+ *
170
+ * 1. Check if the local tx pool contains all of the transactions in the proposal
171
+ * 2. If any transactions are not in the local tx pool, request them from the network
172
+ * 3. If we cannot retrieve them from the network, throw an error
173
+ * @param proposal - The proposal to attest to
174
+ */ async ensureTransactionsAreAvailable(proposal) {
175
+ const txHashes = proposal.payload.txHashes;
176
+ const transactionStatuses = await Promise.all(txHashes.map((txHash)=>this.p2pClient.getTxStatus(txHash)));
177
+ const missingTxs = txHashes.filter((_, index)=>![
178
+ 'pending',
179
+ 'mined'
180
+ ].includes(transactionStatuses[index] ?? ''));
181
+ if (missingTxs.length === 0) {
182
+ return; // All transactions are available
183
+ }
184
+ this.log.verbose(`Missing ${missingTxs.length} transactions in the tx pool, requesting from the network`);
185
+ const requestedTxs = await this.p2pClient.requestTxs(missingTxs);
186
+ if (requestedTxs.some((tx)=>tx === undefined)) {
187
+ throw new TransactionsNotAvailableError(missingTxs);
188
+ }
189
+ }
190
+ async createBlockProposal(header, archive, txs) {
191
+ if (this.previousProposal?.slotNumber.equals(header.globalVariables.slotNumber)) {
192
+ this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
193
+ return Promise.resolve(undefined);
194
+ }
195
+ const newProposal = await this.validationService.createBlockProposal(header, archive, txs);
196
+ this.previousProposal = newProposal;
197
+ return newProposal;
198
+ }
199
+ broadcastBlockProposal(proposal) {
200
+ this.p2pClient.broadcastProposal(proposal);
201
+ }
202
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
203
+ async collectAttestations(proposal, required, deadline) {
204
+ // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
205
+ const slot = proposal.payload.header.globalVariables.slotNumber.toBigInt();
206
+ this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
207
+ if (+deadline < this.dateProvider.now()) {
208
+ this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
209
+ throw new AttestationTimeoutError(required, slot);
210
+ }
211
+ const proposalId = proposal.archive.toString();
212
+ const myAttestation = await this.validationService.attestToProposal(proposal);
213
+ let attestations = [];
214
+ while(true){
215
+ const collectedAttestations = [
216
+ myAttestation,
217
+ ...await this.p2pClient.getAttestationsForSlot(slot, proposalId)
218
+ ];
219
+ const oldSenders = await Promise.all(attestations.map((attestation)=>attestation.getSender()));
220
+ for (const collected of collectedAttestations){
221
+ const collectedSender = await collected.getSender();
222
+ if (!oldSenders.some((sender)=>sender.equals(collectedSender))) {
223
+ this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
224
+ }
225
+ }
226
+ attestations = collectedAttestations;
227
+ if (attestations.length >= required) {
228
+ this.log.verbose(`Collected all ${required} attestations for slot ${slot}`);
229
+ return attestations;
230
+ }
231
+ if (+deadline < this.dateProvider.now()) {
232
+ this.log.error(`Timeout ${deadline.toISOString()} waiting for ${required} attestations for slot ${slot}`);
233
+ throw new AttestationTimeoutError(required, slot);
234
+ }
235
+ this.log.debug(`Collected ${attestations.length} attestations so far`);
236
+ await sleep(this.config.attestationPollingIntervalMs);
237
+ }
238
+ }
239
+ }
240
+ function validatePrivateKey(privateKey) {
241
+ try {
242
+ return Buffer32.fromString(privateKey);
243
+ } catch (error) {
244
+ throw new InvalidValidatorPrivateKeyError();
245
+ }
246
+ }
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@aztec/validator-client",
3
+ "version": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
4
+ "main": "dest/index.js",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./dest/index.js",
8
+ "./config": "./dest/config.js",
9
+ "./errors": "./dest/errors/index.js"
10
+ },
11
+ "bin": "./dest/bin/index.js",
12
+ "typedocOptions": {
13
+ "entryPoints": [
14
+ "./src/index.ts"
15
+ ],
16
+ "name": "Aztec validator",
17
+ "tsconfig": "./tsconfig.json"
18
+ },
19
+ "scripts": {
20
+ "start": "node --no-warnings ./dest/bin",
21
+ "build": "yarn clean && tsc -b",
22
+ "build:dev": "tsc -b --watch",
23
+ "clean": "rm -rf ./dest .tsbuildinfo",
24
+ "formatting": "run -T prettier --check ./src && run -T eslint ./src",
25
+ "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src",
26
+ "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
27
+ },
28
+ "inherits": [
29
+ "../package.common.json"
30
+ ],
31
+ "jest": {
32
+ "moduleNameMapper": {
33
+ "^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
34
+ },
35
+ "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
36
+ "rootDir": "./src",
37
+ "transform": {
38
+ "^.+\\.tsx?$": [
39
+ "@swc/jest",
40
+ {
41
+ "jsc": {
42
+ "parser": {
43
+ "syntax": "typescript",
44
+ "decorators": true
45
+ },
46
+ "transform": {
47
+ "decoratorVersion": "2022-03"
48
+ }
49
+ }
50
+ }
51
+ ]
52
+ },
53
+ "extensionsToTreatAsEsm": [
54
+ ".ts"
55
+ ],
56
+ "reporters": [
57
+ "default"
58
+ ],
59
+ "testTimeout": 30000,
60
+ "setupFiles": [
61
+ "../../foundation/src/jest/setup.mjs"
62
+ ]
63
+ },
64
+ "dependencies": {
65
+ "@aztec/circuit-types": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
66
+ "@aztec/circuits.js": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
67
+ "@aztec/epoch-cache": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
68
+ "@aztec/ethereum": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
69
+ "@aztec/foundation": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
70
+ "@aztec/p2p": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
71
+ "@aztec/telemetry-client": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
72
+ "@aztec/types": "0.75.0-commit.c03ba01a2a4122e43e90d5133ba017e54b90e9d2",
73
+ "koa": "^2.14.2",
74
+ "koa-router": "^12.0.0",
75
+ "tslib": "^2.4.0",
76
+ "viem": "2.22.8"
77
+ },
78
+ "devDependencies": {
79
+ "@jest/globals": "^29.5.0",
80
+ "@types/jest": "^29.5.0",
81
+ "@types/node": "^18.7.23",
82
+ "jest": "^29.5.0",
83
+ "jest-mock-extended": "^3.0.7",
84
+ "ts-node": "^10.9.1",
85
+ "typescript": "^5.0.4"
86
+ },
87
+ "files": [
88
+ "dest",
89
+ "src",
90
+ "!*.test.*"
91
+ ],
92
+ "types": "./dest/index.d.ts",
93
+ "engines": {
94
+ "node": ">=18"
95
+ }
96
+ }
package/src/config.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { NULL_KEY } from '@aztec/ethereum';
2
+ import {
3
+ type ConfigMappingsType,
4
+ booleanConfigHelper,
5
+ getConfigFromMappings,
6
+ numberConfigHelper,
7
+ } from '@aztec/foundation/config';
8
+
9
+ /**
10
+ * The Validator Configuration
11
+ */
12
+ export interface ValidatorClientConfig {
13
+ /** The private key of the validator participating in attestation duties */
14
+ validatorPrivateKey: string;
15
+
16
+ /** Do not run the validator */
17
+ disableValidator: boolean;
18
+
19
+ /** Interval between polling for new attestations from peers */
20
+ attestationPollingIntervalMs: number;
21
+
22
+ /** Re-execute transactions before attesting */
23
+ validatorReexecute: boolean;
24
+ }
25
+
26
+ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientConfig> = {
27
+ validatorPrivateKey: {
28
+ env: 'VALIDATOR_PRIVATE_KEY',
29
+ parseEnv: (val: string) => (val ? `0x${val.replace('0x', '')}` : NULL_KEY),
30
+ description: 'The private key of the validator participating in attestation duties',
31
+ },
32
+ disableValidator: {
33
+ env: 'VALIDATOR_DISABLED',
34
+ description: 'Do not run the validator',
35
+ ...booleanConfigHelper(),
36
+ },
37
+ attestationPollingIntervalMs: {
38
+ env: 'VALIDATOR_ATTESTATIONS_POLLING_INTERVAL_MS',
39
+ description: 'Interval between polling for new attestations',
40
+ ...numberConfigHelper(200),
41
+ },
42
+ validatorReexecute: {
43
+ env: 'VALIDATOR_REEXECUTE',
44
+ description: 'Re-execute transactions before attesting',
45
+ ...booleanConfigHelper(true),
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Returns the prover configuration from the environment variables.
51
+ * Note: If an environment variable is not set, the default value is used.
52
+ * @returns The validator configuration.
53
+ */
54
+ export function getProverEnvVars(): ValidatorClientConfig {
55
+ return getConfigFromMappings<ValidatorClientConfig>(validatorClientConfigMappings);
56
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ BlockAttestation,
3
+ BlockProposal,
4
+ ConsensusPayload,
5
+ SignatureDomainSeparator,
6
+ type TxHash,
7
+ } from '@aztec/circuit-types';
8
+ import { type BlockHeader } from '@aztec/circuits.js';
9
+ import { Buffer32 } from '@aztec/foundation/buffer';
10
+ import { keccak256 } from '@aztec/foundation/crypto';
11
+ import { type Fr } from '@aztec/foundation/fields';
12
+
13
+ import { type ValidatorKeyStore } from '../key_store/interface.js';
14
+
15
+ export class ValidationService {
16
+ constructor(private keyStore: ValidatorKeyStore) {}
17
+
18
+ /**
19
+ * Create a block proposal with the given header, archive, and transactions
20
+ *
21
+ * @param header - The block header
22
+ * @param archive - The archive of the current block
23
+ * @param txs - TxHash[] ordered list of transactions
24
+ *
25
+ * @returns A block proposal signing the above information (not the current implementation!!!)
26
+ */
27
+ createBlockProposal(header: BlockHeader, archive: Fr, txs: TxHash[]): Promise<BlockProposal> {
28
+ const payloadSigner = (payload: Buffer32) => this.keyStore.signMessage(payload);
29
+
30
+ return BlockProposal.createProposalFromSigner(new ConsensusPayload(header, archive, txs), payloadSigner);
31
+ }
32
+
33
+ /**
34
+ * Attest to the given block proposal constructed by the current sequencer
35
+ *
36
+ * NOTE: This is just a blind signing.
37
+ * We assume that the proposal is valid and DA guarantees have been checked previously.
38
+ *
39
+ * @param proposal - The proposal to attest to
40
+ * @returns attestation
41
+ */
42
+ async attestToProposal(proposal: BlockProposal): Promise<BlockAttestation> {
43
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct
44
+
45
+ const buf = Buffer32.fromBuffer(
46
+ keccak256(await proposal.payload.getPayloadToSign(SignatureDomainSeparator.blockAttestation)),
47
+ );
48
+ const sig = await this.keyStore.signMessage(buf);
49
+ return new BlockAttestation(proposal.payload, sig);
50
+ }
51
+ }
@@ -0,0 +1 @@
1
+ export * from './validator.error.js';
@@ -0,0 +1,55 @@
1
+ import { type TxHash } from '@aztec/circuit-types/tx_hash';
2
+
3
+ export class ValidatorError extends Error {
4
+ constructor(message: string) {
5
+ super(`Validator Error: ${message}`);
6
+ }
7
+ }
8
+
9
+ export class InvalidValidatorPrivateKeyError extends ValidatorError {
10
+ constructor() {
11
+ super('Invalid validator private key provided');
12
+ }
13
+ }
14
+
15
+ export class AttestationTimeoutError extends ValidatorError {
16
+ constructor(numberOfRequiredAttestations: number, slot: bigint) {
17
+ super(`Timeout waiting for ${numberOfRequiredAttestations} attestations for slot, ${slot}`);
18
+ }
19
+ }
20
+
21
+ export class TransactionsNotAvailableError extends ValidatorError {
22
+ constructor(txHashes: TxHash[]) {
23
+ super(`Transactions not available: ${txHashes.join(', ')}`);
24
+ }
25
+ }
26
+
27
+ export class FailedToReExecuteTransactionsError extends ValidatorError {
28
+ constructor(txHashes: TxHash[]) {
29
+ super(`Failed to re-execute transactions: ${txHashes.join(', ')}`);
30
+ }
31
+ }
32
+
33
+ export class ReExStateMismatchError extends ValidatorError {
34
+ constructor() {
35
+ super('Re-execution state mismatch');
36
+ }
37
+ }
38
+
39
+ export class ReExFailedTxsError extends ValidatorError {
40
+ constructor(numFailedTxs: number) {
41
+ super(`Re-execution failed to process ${numFailedTxs} txs`);
42
+ }
43
+ }
44
+
45
+ export class ReExTimeoutError extends ValidatorError {
46
+ constructor() {
47
+ super('Re-execution timed out or failed to process all txs in the proposal');
48
+ }
49
+ }
50
+
51
+ export class BlockBuilderNotProvidedError extends ValidatorError {
52
+ constructor() {
53
+ super('Block builder not provided');
54
+ }
55
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { type EpochCache } from '@aztec/epoch-cache';
2
+ import { type DateProvider } from '@aztec/foundation/timer';
3
+ import { type P2P } from '@aztec/p2p';
4
+ import { type TelemetryClient } from '@aztec/telemetry-client';
5
+
6
+ import { generatePrivateKey } from 'viem/accounts';
7
+
8
+ import { type ValidatorClientConfig } from './config.js';
9
+ import { ValidatorClient } from './validator.js';
10
+
11
+ export function createValidatorClient(
12
+ config: ValidatorClientConfig,
13
+ deps: {
14
+ p2pClient: P2P;
15
+ telemetry: TelemetryClient;
16
+ dateProvider: DateProvider;
17
+ epochCache: EpochCache;
18
+ },
19
+ ) {
20
+ if (config.disableValidator) {
21
+ return undefined;
22
+ }
23
+ if (config.validatorPrivateKey === undefined || config.validatorPrivateKey === '') {
24
+ config.validatorPrivateKey = generatePrivateKey();
25
+ }
26
+
27
+ return ValidatorClient.new(config, deps.epochCache, deps.p2pClient, deps.dateProvider, deps.telemetry);
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './config.js';
2
+ export * from './validator.js';
3
+ export * from './factory.js';
@@ -0,0 +1,2 @@
1
+ export * from './interface.js';
2
+ export * from './local_key_store.js';
@@ -0,0 +1,26 @@
1
+ import { type EthAddress } from '@aztec/circuits.js';
2
+ import { type Buffer32 } from '@aztec/foundation/buffer';
3
+ import { type Signature } from '@aztec/foundation/eth-signature';
4
+
5
+ /** Key Store
6
+ *
7
+ * A keystore interface that can be replaced with a local keystore / remote signer service
8
+ */
9
+ export interface ValidatorKeyStore {
10
+ /**
11
+ * Get the address of the signer
12
+ *
13
+ * @returns the address
14
+ */
15
+ getAddress(): EthAddress;
16
+
17
+ sign(message: Buffer32): Promise<Signature>;
18
+ /**
19
+ * Flavor of sign message that followed EIP-712 eth signed message prefix
20
+ * Note: this is only required when we are using ecdsa signatures over secp256k1
21
+ *
22
+ * @param message - The message to sign.
23
+ * @returns The signature.
24
+ */
25
+ signMessage(message: Buffer32): Promise<Signature>;
26
+ }
@@ -0,0 +1,46 @@
1
+ import { type Buffer32 } from '@aztec/foundation/buffer';
2
+ import { Secp256k1Signer } from '@aztec/foundation/crypto';
3
+ import { type EthAddress } from '@aztec/foundation/eth-address';
4
+ import { type Signature } from '@aztec/foundation/eth-signature';
5
+
6
+ import { type ValidatorKeyStore } from './interface.js';
7
+
8
+ /**
9
+ * Local Key Store
10
+ *
11
+ * An implementation of the Key store using an in memory private key.
12
+ */
13
+ export class LocalKeyStore implements ValidatorKeyStore {
14
+ private signer: Secp256k1Signer;
15
+
16
+ constructor(privateKey: Buffer32) {
17
+ this.signer = new Secp256k1Signer(privateKey);
18
+ }
19
+
20
+ /**
21
+ * Get the address of the signer
22
+ *
23
+ * @returns the address
24
+ */
25
+ public getAddress(): EthAddress {
26
+ return this.signer.address;
27
+ }
28
+
29
+ /**
30
+ * Sign a message with the keystore private key
31
+ *
32
+ * @param messageBuffer - The message buffer to sign
33
+ * @return signature
34
+ */
35
+ public sign(digest: Buffer32): Promise<Signature> {
36
+ const signature = this.signer.sign(digest);
37
+
38
+ return Promise.resolve(signature);
39
+ }
40
+
41
+ public signMessage(message: Buffer32): Promise<Signature> {
42
+ // Sign message adds eth sign prefix and hashes before signing
43
+ const signature = this.signer.signMessage(message);
44
+ return Promise.resolve(signature);
45
+ }
46
+ }
package/src/metrics.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { type BlockProposal } from '@aztec/circuit-types';
2
+ import {
3
+ Attributes,
4
+ type Gauge,
5
+ Metrics,
6
+ type TelemetryClient,
7
+ type UpDownCounter,
8
+ ValueType,
9
+ } from '@aztec/telemetry-client';
10
+
11
+ export class ValidatorMetrics {
12
+ private reExecutionTime: Gauge;
13
+ private failedReexecutionCounter: UpDownCounter;
14
+
15
+ constructor(telemetryClient: TelemetryClient) {
16
+ const meter = telemetryClient.getMeter('Validator');
17
+
18
+ this.failedReexecutionCounter = meter.createUpDownCounter(Metrics.VALIDATOR_FAILED_REEXECUTION_COUNT, {
19
+ description: 'The number of failed re-executions',
20
+ unit: 'count',
21
+ valueType: ValueType.INT,
22
+ });
23
+
24
+ this.reExecutionTime = meter.createGauge(Metrics.VALIDATOR_RE_EXECUTION_TIME, {
25
+ description: 'The time taken to re-execute a transaction',
26
+ unit: 'ms',
27
+ valueType: ValueType.DOUBLE,
28
+ });
29
+ }
30
+
31
+ public reExecutionTimer(): () => void {
32
+ const start = performance.now();
33
+ return () => {
34
+ const end = performance.now();
35
+ this.recordReExecutionTime(end - start);
36
+ };
37
+ }
38
+
39
+ public recordReExecutionTime(time: number) {
40
+ this.reExecutionTime.record(time);
41
+ }
42
+
43
+ public async recordFailedReexecution(proposal: BlockProposal) {
44
+ this.failedReexecutionCounter.add(1, {
45
+ [Attributes.STATUS]: 'failed',
46
+ [Attributes.BLOCK_NUMBER]: proposal.payload.header.globalVariables.blockNumber.toString(),
47
+ [Attributes.BLOCK_PROPOSER]: (await proposal.getSender())?.toString(),
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,361 @@
1
+ import { type BlockAttestation, type BlockProposal, type L2Block, type Tx, type TxHash } from '@aztec/circuit-types';
2
+ import { type BlockHeader, type GlobalVariables } from '@aztec/circuits.js';
3
+ import { type EpochCache } from '@aztec/epoch-cache';
4
+ import { Buffer32 } from '@aztec/foundation/buffer';
5
+ import { type Fr } from '@aztec/foundation/fields';
6
+ import { createLogger } from '@aztec/foundation/log';
7
+ import { RunningPromise } from '@aztec/foundation/running-promise';
8
+ import { sleep } from '@aztec/foundation/sleep';
9
+ import { DateProvider, type Timer } from '@aztec/foundation/timer';
10
+ import { type P2P } from '@aztec/p2p';
11
+ import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
12
+ import { type TelemetryClient, WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
13
+
14
+ import { type ValidatorClientConfig } from './config.js';
15
+ import { ValidationService } from './duties/validation_service.js';
16
+ import {
17
+ AttestationTimeoutError,
18
+ BlockBuilderNotProvidedError,
19
+ InvalidValidatorPrivateKeyError,
20
+ ReExFailedTxsError,
21
+ ReExStateMismatchError,
22
+ ReExTimeoutError,
23
+ TransactionsNotAvailableError,
24
+ } from './errors/validator.error.js';
25
+ import { type ValidatorKeyStore } from './key_store/interface.js';
26
+ import { LocalKeyStore } from './key_store/local_key_store.js';
27
+ import { ValidatorMetrics } from './metrics.js';
28
+
29
+ /**
30
+ * Callback function for building a block
31
+ *
32
+ * We reuse the sequencer's block building functionality for re-execution
33
+ */
34
+ type BlockBuilderCallback = (
35
+ txs: Iterable<Tx> | AsyncIterableIterator<Tx>,
36
+ globalVariables: GlobalVariables,
37
+ opts?: { validateOnly?: boolean },
38
+ ) => Promise<{
39
+ block: L2Block;
40
+ publicProcessorDuration: number;
41
+ numTxs: number;
42
+ numFailedTxs: number;
43
+ blockBuildingTimer: Timer;
44
+ }>;
45
+
46
+ export interface Validator {
47
+ start(): Promise<void>;
48
+ registerBlockProposalHandler(): void;
49
+ registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
50
+
51
+ // Block validation responsiblities
52
+ createBlockProposal(header: BlockHeader, archive: Fr, txs: TxHash[]): Promise<BlockProposal | undefined>;
53
+ attestToProposal(proposal: BlockProposal): void;
54
+
55
+ broadcastBlockProposal(proposal: BlockProposal): void;
56
+ collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
57
+ }
58
+
59
+ /**
60
+ * Validator Client
61
+ */
62
+ export class ValidatorClient extends WithTracer implements Validator {
63
+ private validationService: ValidationService;
64
+ private metrics: ValidatorMetrics;
65
+
66
+ // Used to check if we are sending the same proposal twice
67
+ private previousProposal?: BlockProposal;
68
+
69
+ // Callback registered to: sequencer.buildBlock
70
+ private blockBuilder?: BlockBuilderCallback = undefined;
71
+
72
+ private epochCacheUpdateLoop: RunningPromise;
73
+
74
+ private blockProposalValidator: BlockProposalValidator;
75
+
76
+ constructor(
77
+ private keyStore: ValidatorKeyStore,
78
+ private epochCache: EpochCache,
79
+ private p2pClient: P2P,
80
+ private config: ValidatorClientConfig,
81
+ private dateProvider: DateProvider = new DateProvider(),
82
+ telemetry: TelemetryClient = getTelemetryClient(),
83
+ private log = createLogger('validator'),
84
+ ) {
85
+ // Instantiate tracer
86
+ super(telemetry, 'Validator');
87
+ this.metrics = new ValidatorMetrics(telemetry);
88
+
89
+ this.validationService = new ValidationService(keyStore);
90
+
91
+ this.blockProposalValidator = new BlockProposalValidator(epochCache);
92
+
93
+ // Refresh epoch cache every second to trigger commiteeChanged event
94
+ this.epochCacheUpdateLoop = new RunningPromise(
95
+ () =>
96
+ this.epochCache
97
+ .getCommittee()
98
+ .then(() => {})
99
+ .catch(err => log.error('Error updating validator committee', err)),
100
+ log,
101
+ 1000,
102
+ );
103
+
104
+ // Listen to commiteeChanged event to alert operator when their validator has entered the committee
105
+ this.epochCache.on('committeeChanged', (newCommittee, epochNumber) => {
106
+ const me = this.keyStore.getAddress();
107
+ if (newCommittee.some(addr => addr.equals(me))) {
108
+ this.log.info(`Validator ${me.toString()} is on the validator committee for epoch ${epochNumber}`);
109
+ } else {
110
+ this.log.verbose(`Validator ${me.toString()} not on the validator committee for epoch ${epochNumber}`);
111
+ }
112
+ });
113
+
114
+ this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
115
+ }
116
+
117
+ static new(
118
+ config: ValidatorClientConfig,
119
+ epochCache: EpochCache,
120
+ p2pClient: P2P,
121
+ dateProvider: DateProvider = new DateProvider(),
122
+ telemetry: TelemetryClient = getTelemetryClient(),
123
+ ) {
124
+ if (!config.validatorPrivateKey) {
125
+ throw new InvalidValidatorPrivateKeyError();
126
+ }
127
+
128
+ const privateKey = validatePrivateKey(config.validatorPrivateKey);
129
+ const localKeyStore = new LocalKeyStore(privateKey);
130
+
131
+ const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, config, dateProvider, telemetry);
132
+ validator.registerBlockProposalHandler();
133
+ return validator;
134
+ }
135
+
136
+ public async start() {
137
+ // Sync the committee from the smart contract
138
+ // https://github.com/AztecProtocol/aztec-packages/issues/7962
139
+
140
+ const me = this.keyStore.getAddress();
141
+ const inCommittee = await this.epochCache.isInCommittee(me);
142
+ if (inCommittee) {
143
+ this.log.info(`Started validator with address ${me.toString()} in current validator committee`);
144
+ } else {
145
+ this.log.info(`Started validator with address ${me.toString()}`);
146
+ }
147
+ this.epochCacheUpdateLoop.start();
148
+ return Promise.resolve();
149
+ }
150
+
151
+ public async stop() {
152
+ await this.epochCacheUpdateLoop.stop();
153
+ }
154
+
155
+ public registerBlockProposalHandler() {
156
+ const handler = (block: BlockProposal): Promise<BlockAttestation | undefined> => {
157
+ return this.attestToProposal(block);
158
+ };
159
+ this.p2pClient.registerBlockProposalHandler(handler);
160
+ }
161
+
162
+ /**
163
+ * Register a callback function for building a block
164
+ *
165
+ * We reuse the sequencer's block building functionality for re-execution
166
+ */
167
+ public registerBlockBuilder(blockBuilder: BlockBuilderCallback) {
168
+ this.blockBuilder = blockBuilder;
169
+ }
170
+
171
+ async attestToProposal(proposal: BlockProposal): Promise<BlockAttestation | undefined> {
172
+ const slotNumber = proposal.slotNumber.toNumber();
173
+ const proposalInfo = {
174
+ slotNumber,
175
+ blockNumber: proposal.payload.header.globalVariables.blockNumber.toNumber(),
176
+ archive: proposal.payload.archive.toString(),
177
+ txCount: proposal.payload.txHashes.length,
178
+ txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
179
+ };
180
+ this.log.verbose(`Received request to attest for slot ${slotNumber}`);
181
+
182
+ // Check that I am in the committee
183
+ if (!(await this.epochCache.isInCommittee(this.keyStore.getAddress()))) {
184
+ this.log.verbose(`Not in the committee, skipping attestation`);
185
+ return undefined;
186
+ }
187
+
188
+ // Check that the proposal is from the current proposer, or the next proposer.
189
+ const invalidProposal = await this.blockProposalValidator.validate(proposal);
190
+ if (invalidProposal) {
191
+ this.log.verbose(`Proposal is not valid, skipping attestation`);
192
+ return undefined;
193
+ }
194
+
195
+ // Check that all of the tranasctions in the proposal are available in the tx pool before attesting
196
+ this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
197
+ try {
198
+ await this.ensureTransactionsAreAvailable(proposal);
199
+
200
+ if (this.config.validatorReexecute) {
201
+ this.log.verbose(`Re-executing transactions in the proposal before attesting`);
202
+ await this.reExecuteTransactions(proposal);
203
+ }
204
+ } catch (error: any) {
205
+ // If the transactions are not available, then we should not attempt to attest
206
+ if (error instanceof TransactionsNotAvailableError) {
207
+ this.log.error(`Transactions not available, skipping attestation`, error, proposalInfo);
208
+ } else {
209
+ // This branch most commonly be hit if the transactions are available, but the re-execution fails
210
+ // Catch all error handler
211
+ this.log.error(`Failed to attest to proposal`, error, proposalInfo);
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ // Provided all of the above checks pass, we can attest to the proposal
217
+ this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
218
+
219
+ // If the above function does not throw an error, then we can attest to the proposal
220
+ return this.validationService.attestToProposal(proposal);
221
+ }
222
+
223
+ /**
224
+ * Re-execute the transactions in the proposal and check that the state updates match the header state
225
+ * @param proposal - The proposal to re-execute
226
+ */
227
+ async reExecuteTransactions(proposal: BlockProposal) {
228
+ const { header, txHashes } = proposal.payload;
229
+
230
+ const txs = (await Promise.all(txHashes.map(tx => this.p2pClient.getTxByHash(tx)))).filter(
231
+ tx => tx !== undefined,
232
+ ) as Tx[];
233
+
234
+ // If we cannot request all of the transactions, then we should fail
235
+ if (txs.length !== txHashes.length) {
236
+ throw new TransactionsNotAvailableError(txHashes);
237
+ }
238
+
239
+ // Assertion: This check will fail if re-execution is not enabled
240
+ if (this.blockBuilder === undefined) {
241
+ throw new BlockBuilderNotProvidedError();
242
+ }
243
+
244
+ // Use the sequencer's block building logic to re-execute the transactions
245
+ const stopTimer = this.metrics.reExecutionTimer();
246
+ const { block, numFailedTxs } = await this.blockBuilder(txs, header.globalVariables, {
247
+ validateOnly: true,
248
+ });
249
+ stopTimer();
250
+
251
+ this.log.verbose(`Transaction re-execution complete`);
252
+
253
+ if (numFailedTxs > 0) {
254
+ await this.metrics.recordFailedReexecution(proposal);
255
+ throw new ReExFailedTxsError(numFailedTxs);
256
+ }
257
+
258
+ if (block.body.txEffects.length !== txHashes.length) {
259
+ await this.metrics.recordFailedReexecution(proposal);
260
+ throw new ReExTimeoutError();
261
+ }
262
+
263
+ // This function will throw an error if state updates do not match
264
+ if (!block.archive.root.equals(proposal.archive)) {
265
+ await this.metrics.recordFailedReexecution(proposal);
266
+ throw new ReExStateMismatchError();
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Ensure that all of the transactions in the proposal are available in the tx pool before attesting
272
+ *
273
+ * 1. Check if the local tx pool contains all of the transactions in the proposal
274
+ * 2. If any transactions are not in the local tx pool, request them from the network
275
+ * 3. If we cannot retrieve them from the network, throw an error
276
+ * @param proposal - The proposal to attest to
277
+ */
278
+ async ensureTransactionsAreAvailable(proposal: BlockProposal) {
279
+ const txHashes: TxHash[] = proposal.payload.txHashes;
280
+ const transactionStatuses = await Promise.all(txHashes.map(txHash => this.p2pClient.getTxStatus(txHash)));
281
+
282
+ const missingTxs = txHashes.filter((_, index) => !['pending', 'mined'].includes(transactionStatuses[index] ?? ''));
283
+
284
+ if (missingTxs.length === 0) {
285
+ return; // All transactions are available
286
+ }
287
+
288
+ this.log.verbose(`Missing ${missingTxs.length} transactions in the tx pool, requesting from the network`);
289
+
290
+ const requestedTxs = await this.p2pClient.requestTxs(missingTxs);
291
+ if (requestedTxs.some(tx => tx === undefined)) {
292
+ throw new TransactionsNotAvailableError(missingTxs);
293
+ }
294
+ }
295
+
296
+ async createBlockProposal(header: BlockHeader, archive: Fr, txs: TxHash[]): Promise<BlockProposal | undefined> {
297
+ if (this.previousProposal?.slotNumber.equals(header.globalVariables.slotNumber)) {
298
+ this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
299
+ return Promise.resolve(undefined);
300
+ }
301
+
302
+ const newProposal = await this.validationService.createBlockProposal(header, archive, txs);
303
+ this.previousProposal = newProposal;
304
+ return newProposal;
305
+ }
306
+
307
+ broadcastBlockProposal(proposal: BlockProposal): void {
308
+ this.p2pClient.broadcastProposal(proposal);
309
+ }
310
+
311
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
312
+ async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
313
+ // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
314
+ const slot = proposal.payload.header.globalVariables.slotNumber.toBigInt();
315
+ this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
316
+
317
+ if (+deadline < this.dateProvider.now()) {
318
+ this.log.error(
319
+ `Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`,
320
+ );
321
+ throw new AttestationTimeoutError(required, slot);
322
+ }
323
+
324
+ const proposalId = proposal.archive.toString();
325
+ const myAttestation = await this.validationService.attestToProposal(proposal);
326
+
327
+ let attestations: BlockAttestation[] = [];
328
+ while (true) {
329
+ const collectedAttestations = [myAttestation, ...(await this.p2pClient.getAttestationsForSlot(slot, proposalId))];
330
+ const oldSenders = await Promise.all(attestations.map(attestation => attestation.getSender()));
331
+ for (const collected of collectedAttestations) {
332
+ const collectedSender = await collected.getSender();
333
+ if (!oldSenders.some(sender => sender.equals(collectedSender))) {
334
+ this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
335
+ }
336
+ }
337
+ attestations = collectedAttestations;
338
+
339
+ if (attestations.length >= required) {
340
+ this.log.verbose(`Collected all ${required} attestations for slot ${slot}`);
341
+ return attestations;
342
+ }
343
+
344
+ if (+deadline < this.dateProvider.now()) {
345
+ this.log.error(`Timeout ${deadline.toISOString()} waiting for ${required} attestations for slot ${slot}`);
346
+ throw new AttestationTimeoutError(required, slot);
347
+ }
348
+
349
+ this.log.debug(`Collected ${attestations.length} attestations so far`);
350
+ await sleep(this.config.attestationPollingIntervalMs);
351
+ }
352
+ }
353
+ }
354
+
355
+ function validatePrivateKey(privateKey: string): Buffer32 {
356
+ try {
357
+ return Buffer32.fromString(privateKey);
358
+ } catch (error) {
359
+ throw new InvalidValidatorPrivateKeyError();
360
+ }
361
+ }