@aztec/validator-client 0.86.0 → 0.87.0
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/duties/validation_service.d.ts +4 -3
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +6 -3
- package/dest/factory.d.ts +2 -0
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -1
- package/dest/validator.d.ts +16 -15
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +109 -37
- package/package.json +11 -10
- package/src/duties/validation_service.ts +26 -5
- package/src/factory.ts +10 -1
- package/src/validator.ts +170 -46
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Fr } from '@aztec/foundation/fields';
|
|
2
|
-
import { BlockAttestation, BlockProposal } from '@aztec/stdlib/p2p';
|
|
3
|
-
import type {
|
|
2
|
+
import { BlockAttestation, BlockProposal, type BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
3
|
+
import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
|
|
4
4
|
import type { ValidatorKeyStore } from '../key_store/interface.js';
|
|
5
5
|
export declare class ValidationService {
|
|
6
6
|
private keyStore;
|
|
@@ -8,13 +8,14 @@ export declare class ValidationService {
|
|
|
8
8
|
/**
|
|
9
9
|
* Create a block proposal with the given header, archive, and transactions
|
|
10
10
|
*
|
|
11
|
+
* @param blockNumber - The block number this proposal is for
|
|
11
12
|
* @param header - The block header
|
|
12
13
|
* @param archive - The archive of the current block
|
|
13
14
|
* @param txs - TxHash[] ordered list of transactions
|
|
14
15
|
*
|
|
15
16
|
* @returns A block proposal signing the above information (not the current implementation!!!)
|
|
16
17
|
*/
|
|
17
|
-
createBlockProposal(header:
|
|
18
|
+
createBlockProposal(blockNumber: Fr, header: ProposedBlockHeader, archive: Fr, stateReference: StateReference, txs: Tx[], options: BlockProposalOptions): Promise<BlockProposal>;
|
|
18
19
|
/**
|
|
19
20
|
* Attest to the given block proposal constructed by the current sequencer
|
|
20
21
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation_service.d.ts","sourceRoot":"","sources":["../../src/duties/validation_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,
|
|
1
|
+
{"version":3,"file":"validation_service.d.ts","sourceRoot":"","sources":["../../src/duties/validation_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,KAAK,oBAAoB,EAG1B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,iBAAiB;IAE/C;;;;;;;;;OASG;IACG,mBAAmB,CACvB,WAAW,EAAE,EAAE,EACf,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,EAAE,EACX,cAAc,EAAE,cAAc,EAC9B,GAAG,EAAE,EAAE,EAAE,EACT,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC;IAazB;;;;;;;;OAQG;IACG,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAS3E"}
|
|
@@ -9,14 +9,17 @@ export class ValidationService {
|
|
|
9
9
|
/**
|
|
10
10
|
* Create a block proposal with the given header, archive, and transactions
|
|
11
11
|
*
|
|
12
|
+
* @param blockNumber - The block number this proposal is for
|
|
12
13
|
* @param header - The block header
|
|
13
14
|
* @param archive - The archive of the current block
|
|
14
15
|
* @param txs - TxHash[] ordered list of transactions
|
|
15
16
|
*
|
|
16
17
|
* @returns A block proposal signing the above information (not the current implementation!!!)
|
|
17
|
-
*/ createBlockProposal(header, archive, txs) {
|
|
18
|
+
*/ async createBlockProposal(blockNumber, header, archive, stateReference, txs, options) {
|
|
18
19
|
const payloadSigner = (payload)=>this.keyStore.signMessage(payload);
|
|
19
|
-
|
|
20
|
+
// TODO: check if this is calculated earlier / can not be recomputed
|
|
21
|
+
const txHashes = await Promise.all(txs.map((tx)=>tx.getTxHash()));
|
|
22
|
+
return BlockProposal.createProposalFromSigner(blockNumber, new ConsensusPayload(header, archive, stateReference, txHashes), options.publishFullTxs ? txs : undefined, payloadSigner);
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Attest to the given block proposal constructed by the current sequencer
|
|
@@ -30,6 +33,6 @@ export class ValidationService {
|
|
|
30
33
|
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct
|
|
31
34
|
const buf = Buffer32.fromBuffer(keccak256(proposal.payload.getPayloadToSign(SignatureDomainSeparator.blockAttestation)));
|
|
32
35
|
const sig = await this.keyStore.signMessage(buf);
|
|
33
|
-
return new BlockAttestation(proposal.payload, sig);
|
|
36
|
+
return new BlockAttestation(proposal.blockNumber, proposal.payload, sig);
|
|
34
37
|
}
|
|
35
38
|
}
|
package/dest/factory.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
2
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
3
3
|
import type { P2P } from '@aztec/p2p';
|
|
4
|
+
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
4
5
|
import type { TelemetryClient } from '@aztec/telemetry-client';
|
|
5
6
|
import type { ValidatorClientConfig } from './config.js';
|
|
6
7
|
import { ValidatorClient } from './validator.js';
|
|
7
8
|
export declare function createValidatorClient(config: ValidatorClientConfig, deps: {
|
|
8
9
|
p2pClient: P2P;
|
|
10
|
+
blockSource: L2BlockSource;
|
|
9
11
|
telemetry: TelemetryClient;
|
|
10
12
|
dateProvider: DateProvider;
|
|
11
13
|
epochCache: EpochCache;
|
package/dest/factory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE;IACJ,SAAS,EAAE,GAAG,CAAC;IACf,SAAS,EAAE,eAAe,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACxB,+
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,qBAAqB,EAC7B,IAAI,EAAE;IACJ,SAAS,EAAE,GAAG,CAAC;IACf,WAAW,EAAE,aAAa,CAAC;IAC3B,SAAS,EAAE,eAAe,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACxB,+BAiBF"}
|
package/dest/factory.js
CHANGED
|
@@ -7,5 +7,5 @@ export function createValidatorClient(config, deps) {
|
|
|
7
7
|
if (config.validatorPrivateKey === undefined || config.validatorPrivateKey === '') {
|
|
8
8
|
config.validatorPrivateKey = generatePrivateKey();
|
|
9
9
|
}
|
|
10
|
-
return ValidatorClient.new(config, deps.epochCache, deps.p2pClient, deps.dateProvider, deps.telemetry);
|
|
10
|
+
return ValidatorClient.new(config, deps.epochCache, deps.p2pClient, deps.blockSource, deps.dateProvider, deps.telemetry);
|
|
11
11
|
}
|
package/dest/validator.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
2
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
-
import
|
|
3
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
4
4
|
import { DateProvider, type Timer } from '@aztec/foundation/timer';
|
|
5
5
|
import type { P2P } from '@aztec/p2p';
|
|
6
|
-
import type { L2Block } from '@aztec/stdlib/block';
|
|
7
|
-
import type { BlockAttestation, BlockProposal } from '@aztec/stdlib/p2p';
|
|
8
|
-
import type {
|
|
6
|
+
import type { L2Block, L2BlockSource } from '@aztec/stdlib/block';
|
|
7
|
+
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
8
|
+
import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
|
|
9
9
|
import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client';
|
|
10
10
|
import type { ValidatorClientConfig } from './config.js';
|
|
11
11
|
import type { ValidatorKeyStore } from './key_store/interface.js';
|
|
@@ -14,7 +14,7 @@ import type { ValidatorKeyStore } from './key_store/interface.js';
|
|
|
14
14
|
*
|
|
15
15
|
* We reuse the sequencer's block building functionality for re-execution
|
|
16
16
|
*/
|
|
17
|
-
type BlockBuilderCallback = (txs: Iterable<Tx> | AsyncIterableIterator<Tx>,
|
|
17
|
+
type BlockBuilderCallback = (blockNumber: Fr, header: ProposedBlockHeader, txs: Iterable<Tx> | AsyncIterableIterator<Tx>, opts?: {
|
|
18
18
|
validateOnly?: boolean;
|
|
19
19
|
}) => Promise<{
|
|
20
20
|
block: L2Block;
|
|
@@ -27,9 +27,9 @@ export interface Validator {
|
|
|
27
27
|
start(): Promise<void>;
|
|
28
28
|
registerBlockProposalHandler(): void;
|
|
29
29
|
registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
|
|
30
|
-
createBlockProposal(header:
|
|
31
|
-
attestToProposal(proposal: BlockProposal):
|
|
32
|
-
broadcastBlockProposal(proposal: BlockProposal): void
|
|
30
|
+
createBlockProposal(blockNumber: Fr, header: ProposedBlockHeader, archive: Fr, stateReference: StateReference, txs: Tx[], options: BlockProposalOptions): Promise<BlockProposal | undefined>;
|
|
31
|
+
attestToProposal(proposal: BlockProposal): Promise<BlockAttestation | undefined>;
|
|
32
|
+
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
33
33
|
collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
@@ -39,6 +39,7 @@ export declare class ValidatorClient extends WithTracer implements Validator {
|
|
|
39
39
|
private keyStore;
|
|
40
40
|
private epochCache;
|
|
41
41
|
private p2pClient;
|
|
42
|
+
private blockSource;
|
|
42
43
|
private config;
|
|
43
44
|
private dateProvider;
|
|
44
45
|
private log;
|
|
@@ -50,9 +51,9 @@ export declare class ValidatorClient extends WithTracer implements Validator {
|
|
|
50
51
|
private lastEpoch;
|
|
51
52
|
private epochCacheUpdateLoop;
|
|
52
53
|
private blockProposalValidator;
|
|
53
|
-
constructor(keyStore: ValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P, config: ValidatorClientConfig, dateProvider?: DateProvider, telemetry?: TelemetryClient, log?: import("@aztec/foundation/log").Logger);
|
|
54
|
-
private
|
|
55
|
-
static new(config: ValidatorClientConfig, epochCache: EpochCache, p2pClient: P2P, dateProvider?: DateProvider, telemetry?: TelemetryClient): ValidatorClient;
|
|
54
|
+
constructor(keyStore: ValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P, blockSource: L2BlockSource, config: ValidatorClientConfig, dateProvider?: DateProvider, telemetry?: TelemetryClient, log?: import("@aztec/foundation/log").Logger);
|
|
55
|
+
private handleEpochCommitteeUpdate;
|
|
56
|
+
static new(config: ValidatorClientConfig, epochCache: EpochCache, p2pClient: P2P, blockSource: L2BlockSource, dateProvider?: DateProvider, telemetry?: TelemetryClient): ValidatorClient;
|
|
56
57
|
getValidatorAddress(): EthAddress;
|
|
57
58
|
start(): Promise<void>;
|
|
58
59
|
stop(): Promise<void>;
|
|
@@ -68,7 +69,7 @@ export declare class ValidatorClient extends WithTracer implements Validator {
|
|
|
68
69
|
* Re-execute the transactions in the proposal and check that the state updates match the header state
|
|
69
70
|
* @param proposal - The proposal to re-execute
|
|
70
71
|
*/
|
|
71
|
-
reExecuteTransactions(proposal: BlockProposal): Promise<void>;
|
|
72
|
+
reExecuteTransactions(proposal: BlockProposal, txs: Tx[]): Promise<void>;
|
|
72
73
|
/**
|
|
73
74
|
* Ensure that all of the transactions in the proposal are available in the tx pool before attesting
|
|
74
75
|
*
|
|
@@ -77,9 +78,9 @@ export declare class ValidatorClient extends WithTracer implements Validator {
|
|
|
77
78
|
* 3. If we cannot retrieve them from the network, throw an error
|
|
78
79
|
* @param proposal - The proposal to attest to
|
|
79
80
|
*/
|
|
80
|
-
ensureTransactionsAreAvailable(proposal: BlockProposal): Promise<
|
|
81
|
-
createBlockProposal(header:
|
|
82
|
-
broadcastBlockProposal(proposal: BlockProposal): void
|
|
81
|
+
ensureTransactionsAreAvailable(proposal: BlockProposal): Promise<Tx[]>;
|
|
82
|
+
createBlockProposal(blockNumber: Fr, header: ProposedBlockHeader, archive: Fr, stateReference: StateReference, txs: Tx[], options: BlockProposalOptions): Promise<BlockProposal | undefined>;
|
|
83
|
+
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
83
84
|
collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
|
|
84
85
|
private doAttestToProposal;
|
|
85
86
|
}
|
package/dest/validator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"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"}
|
package/dest/validator.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
1
2
|
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
2
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
3
4
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
@@ -15,6 +16,7 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
15
16
|
keyStore;
|
|
16
17
|
epochCache;
|
|
17
18
|
p2pClient;
|
|
19
|
+
blockSource;
|
|
18
20
|
config;
|
|
19
21
|
dateProvider;
|
|
20
22
|
log;
|
|
@@ -28,18 +30,18 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
28
30
|
lastEpoch;
|
|
29
31
|
epochCacheUpdateLoop;
|
|
30
32
|
blockProposalValidator;
|
|
31
|
-
constructor(keyStore, epochCache, p2pClient, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
33
|
+
constructor(keyStore, epochCache, p2pClient, blockSource, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
32
34
|
// Instantiate tracer
|
|
33
|
-
super(telemetry, 'Validator'), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.config = config, this.dateProvider = dateProvider, this.log = log, this.blockBuilder = undefined;
|
|
35
|
+
super(telemetry, 'Validator'), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.config = config, this.dateProvider = dateProvider, this.log = log, this.blockBuilder = undefined;
|
|
34
36
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
35
37
|
this.validationService = new ValidationService(keyStore);
|
|
36
38
|
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
37
|
-
// Refresh epoch cache every second to trigger alert if participation in
|
|
39
|
+
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
38
40
|
this.myAddress = this.keyStore.getAddress();
|
|
39
|
-
this.epochCacheUpdateLoop = new RunningPromise(this.
|
|
41
|
+
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
40
42
|
this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
|
|
41
43
|
}
|
|
42
|
-
async
|
|
44
|
+
async handleEpochCommitteeUpdate() {
|
|
43
45
|
try {
|
|
44
46
|
const { committee, epoch } = await this.epochCache.getCommittee('now');
|
|
45
47
|
if (epoch !== this.lastEpoch) {
|
|
@@ -55,13 +57,13 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
55
57
|
this.log.error(`Error updating epoch committee`, err);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
|
-
static new(config, epochCache, p2pClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
60
|
+
static new(config, epochCache, p2pClient, blockSource, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
59
61
|
if (!config.validatorPrivateKey) {
|
|
60
62
|
throw new InvalidValidatorPrivateKeyError();
|
|
61
63
|
}
|
|
62
64
|
const privateKey = validatePrivateKey(config.validatorPrivateKey);
|
|
63
65
|
const localKeyStore = new LocalKeyStore(privateKey);
|
|
64
|
-
const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, config, dateProvider, telemetry);
|
|
66
|
+
const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, blockSource, config, dateProvider, telemetry);
|
|
65
67
|
validator.registerBlockProposalHandler();
|
|
66
68
|
return validator;
|
|
67
69
|
}
|
|
@@ -99,9 +101,10 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
99
101
|
}
|
|
100
102
|
async attestToProposal(proposal) {
|
|
101
103
|
const slotNumber = proposal.slotNumber.toNumber();
|
|
104
|
+
const blockNumber = proposal.blockNumber.toNumber();
|
|
102
105
|
const proposalInfo = {
|
|
103
106
|
slotNumber,
|
|
104
|
-
blockNumber
|
|
107
|
+
blockNumber,
|
|
105
108
|
archive: proposal.payload.archive.toString(),
|
|
106
109
|
txCount: proposal.payload.txHashes.length,
|
|
107
110
|
txHashes: proposal.payload.txHashes.map((txHash)=>txHash.toString())
|
|
@@ -119,20 +122,38 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
119
122
|
this.metrics.incFailedAttestations('invalid_proposal');
|
|
120
123
|
return undefined;
|
|
121
124
|
}
|
|
125
|
+
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
126
|
+
// Q: Should we move this to the block proposal validator? If there, then p2p would check it
|
|
127
|
+
// before re-broadcasting it. This means that proposals built on top of an L1-reorgd-out block
|
|
128
|
+
// would not be rebroadcasted. But it also means that nodes that have not fully synced would
|
|
129
|
+
// not rebroadcast the proposal.
|
|
130
|
+
if (blockNumber > INITIAL_L2_BLOCK_NUM) {
|
|
131
|
+
const parentBlock = await this.blockSource.getBlock(blockNumber - 1);
|
|
132
|
+
if (parentBlock === undefined) {
|
|
133
|
+
this.log.verbose(`Parent block for ${blockNumber} not found, skipping attestation`);
|
|
134
|
+
this.metrics.incFailedAttestations('parent_block_not_found');
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
138
|
+
this.log.verbose(`Parent block archive root for proposal does not match, skipping attestation`, {
|
|
139
|
+
proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
|
|
140
|
+
parentBlockArchiveRoot: parentBlock.archive.root.toString(),
|
|
141
|
+
...proposalInfo
|
|
142
|
+
});
|
|
143
|
+
this.metrics.incFailedAttestations('parent_block_does_not_match');
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
122
147
|
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
123
148
|
this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
|
|
124
149
|
try {
|
|
125
|
-
await this.ensureTransactionsAreAvailable(proposal);
|
|
150
|
+
const txs = await this.ensureTransactionsAreAvailable(proposal);
|
|
126
151
|
if (this.config.validatorReexecute) {
|
|
127
152
|
this.log.verbose(`Re-executing transactions in the proposal before attesting`);
|
|
128
|
-
await this.reExecuteTransactions(proposal);
|
|
153
|
+
await this.reExecuteTransactions(proposal, txs);
|
|
129
154
|
}
|
|
130
155
|
} catch (error) {
|
|
131
|
-
|
|
132
|
-
this.metrics.incFailedAttestations(error.name);
|
|
133
|
-
} else {
|
|
134
|
-
this.metrics.incFailedAttestations('unknown');
|
|
135
|
-
}
|
|
156
|
+
this.metrics.incFailedAttestations(error instanceof Error ? error.name : 'unknown');
|
|
136
157
|
// If the transactions are not available, then we should not attempt to attest
|
|
137
158
|
if (error instanceof TransactionsNotAvailableError) {
|
|
138
159
|
this.log.error(`Transactions not available, skipping attestation`, error, proposalInfo);
|
|
@@ -152,12 +173,13 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
152
173
|
/**
|
|
153
174
|
* Re-execute the transactions in the proposal and check that the state updates match the header state
|
|
154
175
|
* @param proposal - The proposal to re-execute
|
|
155
|
-
*/ async reExecuteTransactions(proposal) {
|
|
176
|
+
*/ async reExecuteTransactions(proposal, txs) {
|
|
156
177
|
const { header, txHashes } = proposal.payload;
|
|
157
|
-
|
|
158
|
-
// If we cannot request all of the transactions, then we should fail
|
|
178
|
+
// If we do not have all of the transactions, then we should fail
|
|
159
179
|
if (txs.length !== txHashes.length) {
|
|
160
|
-
|
|
180
|
+
const foundTxHashes = await Promise.all(txs.map(async (tx)=>await tx.getTxHash()));
|
|
181
|
+
const missingTxHashes = txHashes.filter((txHash)=>!foundTxHashes.includes(txHash));
|
|
182
|
+
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
161
183
|
}
|
|
162
184
|
// Assertion: This check will fail if re-execution is not enabled
|
|
163
185
|
if (this.blockBuilder === undefined) {
|
|
@@ -165,7 +187,7 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
165
187
|
}
|
|
166
188
|
// Use the sequencer's block building logic to re-execute the transactions
|
|
167
189
|
const stopTimer = this.metrics.reExecutionTimer();
|
|
168
|
-
const { block, numFailedTxs } = await this.blockBuilder(
|
|
190
|
+
const { block, numFailedTxs } = await this.blockBuilder(proposal.blockNumber, header, txs, {
|
|
169
191
|
validateOnly: true
|
|
170
192
|
});
|
|
171
193
|
stopTimer();
|
|
@@ -192,37 +214,87 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
192
214
|
* 3. If we cannot retrieve them from the network, throw an error
|
|
193
215
|
* @param proposal - The proposal to attest to
|
|
194
216
|
*/ async ensureTransactionsAreAvailable(proposal) {
|
|
217
|
+
if (proposal.payload.txHashes.length === 0) {
|
|
218
|
+
this.log.verbose(`Received block proposal with no transactions, skipping transaction availability check`);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
// Is this a new style proposal?
|
|
222
|
+
if (proposal.txs && proposal.txs.length > 0 && proposal.txs.length === proposal.payload.txHashes.length) {
|
|
223
|
+
// Yes, any txs that we already have we should use
|
|
224
|
+
this.log.info(`Using new style proposal with ${proposal.txs.length} transactions`);
|
|
225
|
+
// Request from the pool based on the signed hashes in the payload
|
|
226
|
+
const hashesFromPayload = proposal.payload.txHashes;
|
|
227
|
+
const txsToUse = await this.p2pClient.getTxsByHashFromPool(hashesFromPayload);
|
|
228
|
+
const missingTxs = txsToUse.filter((tx)=>tx === undefined).length;
|
|
229
|
+
if (missingTxs > 0) {
|
|
230
|
+
this.log.verbose(`Missing ${missingTxs}/${hashesFromPayload.length} transactions in the tx pool, will attempt to take from the proposal`);
|
|
231
|
+
}
|
|
232
|
+
let usedFromProposal = 0;
|
|
233
|
+
// Fill any holes with txs in the proposal, provided their hash matches the hash in the payload
|
|
234
|
+
for(let i = 0; i < txsToUse.length; i++){
|
|
235
|
+
if (txsToUse[i] === undefined) {
|
|
236
|
+
// We don't have the transaction, take from the proposal, provided the hash is the same
|
|
237
|
+
const hashOfTxInProposal = await proposal.txs[i].getTxHash();
|
|
238
|
+
if (hashOfTxInProposal.equals(hashesFromPayload[i])) {
|
|
239
|
+
// Hash is equal, we can use the tx from the proposal
|
|
240
|
+
txsToUse[i] = proposal.txs[i];
|
|
241
|
+
usedFromProposal++;
|
|
242
|
+
} else {
|
|
243
|
+
this.log.warn(`Unable to take tx: ${hashOfTxInProposal.toString()} from the proposal, it does not match payload hash: ${hashesFromPayload[i].toString()}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// See if we still have any holes, if there are then we were not successful and will try the old method
|
|
248
|
+
if (txsToUse.some((tx)=>tx === undefined)) {
|
|
249
|
+
this.log.warn(`Failed to use transactions from proposal. Falling back to old proposal logic`);
|
|
250
|
+
} else {
|
|
251
|
+
this.log.info(`Successfully used ${usedFromProposal}/${hashesFromPayload.length} transactions from the proposal`);
|
|
252
|
+
await this.p2pClient.validate(txsToUse);
|
|
253
|
+
return txsToUse;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.log.info(`Using old style proposal with ${proposal.payload.txHashes.length} transactions`);
|
|
257
|
+
// Old style proposal, we will perform a request by hash from pool
|
|
258
|
+
// This will request from network any txs that are missing
|
|
195
259
|
const txHashes = proposal.payload.txHashes;
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (missingTxs.length === 0) {
|
|
202
|
-
return; // All transactions are available
|
|
260
|
+
// This part is just for logging that we are requesting from the network
|
|
261
|
+
const availability = await this.p2pClient.hasTxsInPool(txHashes);
|
|
262
|
+
const notAvailable = availability.filter((availability)=>availability === false);
|
|
263
|
+
if (notAvailable.length) {
|
|
264
|
+
this.log.verbose(`Missing ${notAvailable.length} transactions in the tx pool, will need to request from the network`);
|
|
203
265
|
}
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
266
|
+
// This will request from the network any txs that are missing
|
|
267
|
+
const retrievedTxs = await this.p2pClient.getTxsByHash(txHashes);
|
|
268
|
+
const missingTxs = retrievedTxs.map((tx, index)=>{
|
|
269
|
+
// Return the hash of any that we did not get
|
|
270
|
+
if (tx === undefined) {
|
|
271
|
+
return txHashes[index];
|
|
272
|
+
} else {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}).filter((hash)=>hash !== undefined);
|
|
276
|
+
if (missingTxs.length > 0) {
|
|
207
277
|
throw new TransactionsNotAvailableError(missingTxs);
|
|
208
278
|
}
|
|
279
|
+
await this.p2pClient.validate(retrievedTxs);
|
|
280
|
+
return retrievedTxs;
|
|
209
281
|
}
|
|
210
|
-
async createBlockProposal(header, archive, txs) {
|
|
211
|
-
if (this.previousProposal?.slotNumber.equals(header.
|
|
282
|
+
async createBlockProposal(blockNumber, header, archive, stateReference, txs, options) {
|
|
283
|
+
if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
|
|
212
284
|
this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
213
285
|
return Promise.resolve(undefined);
|
|
214
286
|
}
|
|
215
|
-
const newProposal = await this.validationService.createBlockProposal(header, archive, txs);
|
|
287
|
+
const newProposal = await this.validationService.createBlockProposal(blockNumber, header, archive, stateReference, txs, options);
|
|
216
288
|
this.previousProposal = newProposal;
|
|
217
289
|
return newProposal;
|
|
218
290
|
}
|
|
219
|
-
broadcastBlockProposal(proposal) {
|
|
220
|
-
this.p2pClient.broadcastProposal(proposal);
|
|
291
|
+
async broadcastBlockProposal(proposal) {
|
|
292
|
+
await this.p2pClient.broadcastProposal(proposal);
|
|
221
293
|
}
|
|
222
294
|
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
|
|
223
295
|
async collectAttestations(proposal, required, deadline) {
|
|
224
296
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
225
|
-
const slot = proposal.payload.header.
|
|
297
|
+
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
226
298
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
227
299
|
if (+deadline < this.dateProvider.now()) {
|
|
228
300
|
this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
|
|
@@ -263,7 +335,7 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
263
335
|
function validatePrivateKey(privateKey) {
|
|
264
336
|
try {
|
|
265
337
|
return Buffer32.fromString(privateKey);
|
|
266
|
-
} catch
|
|
338
|
+
} catch {
|
|
267
339
|
throw new InvalidValidatorPrivateKeyError();
|
|
268
340
|
}
|
|
269
341
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -60,12 +60,13 @@
|
|
|
60
60
|
]
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@aztec/
|
|
64
|
-
"@aztec/
|
|
65
|
-
"@aztec/
|
|
66
|
-
"@aztec/
|
|
67
|
-
"@aztec/
|
|
68
|
-
"@aztec/
|
|
63
|
+
"@aztec/constants": "0.87.0",
|
|
64
|
+
"@aztec/epoch-cache": "0.87.0",
|
|
65
|
+
"@aztec/ethereum": "0.87.0",
|
|
66
|
+
"@aztec/foundation": "0.87.0",
|
|
67
|
+
"@aztec/p2p": "0.87.0",
|
|
68
|
+
"@aztec/stdlib": "0.87.0",
|
|
69
|
+
"@aztec/telemetry-client": "0.87.0",
|
|
69
70
|
"koa": "^2.16.1",
|
|
70
71
|
"koa-router": "^12.0.0",
|
|
71
72
|
"tslib": "^2.4.0",
|
|
@@ -74,11 +75,11 @@
|
|
|
74
75
|
"devDependencies": {
|
|
75
76
|
"@jest/globals": "^29.5.0",
|
|
76
77
|
"@types/jest": "^29.5.0",
|
|
77
|
-
"@types/node": "^
|
|
78
|
+
"@types/node": "^22.15.17",
|
|
78
79
|
"jest": "^29.5.0",
|
|
79
80
|
"jest-mock-extended": "^3.0.7",
|
|
80
81
|
"ts-node": "^10.9.1",
|
|
81
|
-
"typescript": "^5.
|
|
82
|
+
"typescript": "^5.3.3"
|
|
82
83
|
},
|
|
83
84
|
"files": [
|
|
84
85
|
"dest",
|
|
@@ -87,6 +88,6 @@
|
|
|
87
88
|
],
|
|
88
89
|
"types": "./dest/index.d.ts",
|
|
89
90
|
"engines": {
|
|
90
|
-
"node": ">=
|
|
91
|
+
"node": ">=20.10"
|
|
91
92
|
}
|
|
92
93
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
2
2
|
import { keccak256 } from '@aztec/foundation/crypto';
|
|
3
3
|
import type { Fr } from '@aztec/foundation/fields';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
BlockAttestation,
|
|
6
|
+
BlockProposal,
|
|
7
|
+
type BlockProposalOptions,
|
|
8
|
+
ConsensusPayload,
|
|
9
|
+
SignatureDomainSeparator,
|
|
10
|
+
} from '@aztec/stdlib/p2p';
|
|
11
|
+
import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
|
|
6
12
|
|
|
7
13
|
import type { ValidatorKeyStore } from '../key_store/interface.js';
|
|
8
14
|
|
|
@@ -12,16 +18,31 @@ export class ValidationService {
|
|
|
12
18
|
/**
|
|
13
19
|
* Create a block proposal with the given header, archive, and transactions
|
|
14
20
|
*
|
|
21
|
+
* @param blockNumber - The block number this proposal is for
|
|
15
22
|
* @param header - The block header
|
|
16
23
|
* @param archive - The archive of the current block
|
|
17
24
|
* @param txs - TxHash[] ordered list of transactions
|
|
18
25
|
*
|
|
19
26
|
* @returns A block proposal signing the above information (not the current implementation!!!)
|
|
20
27
|
*/
|
|
21
|
-
createBlockProposal(
|
|
28
|
+
async createBlockProposal(
|
|
29
|
+
blockNumber: Fr,
|
|
30
|
+
header: ProposedBlockHeader,
|
|
31
|
+
archive: Fr,
|
|
32
|
+
stateReference: StateReference,
|
|
33
|
+
txs: Tx[],
|
|
34
|
+
options: BlockProposalOptions,
|
|
35
|
+
): Promise<BlockProposal> {
|
|
22
36
|
const payloadSigner = (payload: Buffer32) => this.keyStore.signMessage(payload);
|
|
37
|
+
// TODO: check if this is calculated earlier / can not be recomputed
|
|
38
|
+
const txHashes = await Promise.all(txs.map(tx => tx.getTxHash()));
|
|
23
39
|
|
|
24
|
-
return BlockProposal.createProposalFromSigner(
|
|
40
|
+
return BlockProposal.createProposalFromSigner(
|
|
41
|
+
blockNumber,
|
|
42
|
+
new ConsensusPayload(header, archive, stateReference, txHashes),
|
|
43
|
+
options.publishFullTxs ? txs : undefined,
|
|
44
|
+
payloadSigner,
|
|
45
|
+
);
|
|
25
46
|
}
|
|
26
47
|
|
|
27
48
|
/**
|
|
@@ -40,6 +61,6 @@ export class ValidationService {
|
|
|
40
61
|
keccak256(proposal.payload.getPayloadToSign(SignatureDomainSeparator.blockAttestation)),
|
|
41
62
|
);
|
|
42
63
|
const sig = await this.keyStore.signMessage(buf);
|
|
43
|
-
return new BlockAttestation(proposal.payload, sig);
|
|
64
|
+
return new BlockAttestation(proposal.blockNumber, proposal.payload, sig);
|
|
44
65
|
}
|
|
45
66
|
}
|
package/src/factory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
2
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
3
3
|
import type { P2P } from '@aztec/p2p';
|
|
4
|
+
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
4
5
|
import type { TelemetryClient } from '@aztec/telemetry-client';
|
|
5
6
|
|
|
6
7
|
import { generatePrivateKey } from 'viem/accounts';
|
|
@@ -12,6 +13,7 @@ export function createValidatorClient(
|
|
|
12
13
|
config: ValidatorClientConfig,
|
|
13
14
|
deps: {
|
|
14
15
|
p2pClient: P2P;
|
|
16
|
+
blockSource: L2BlockSource;
|
|
15
17
|
telemetry: TelemetryClient;
|
|
16
18
|
dateProvider: DateProvider;
|
|
17
19
|
epochCache: EpochCache;
|
|
@@ -24,5 +26,12 @@ export function createValidatorClient(
|
|
|
24
26
|
config.validatorPrivateKey = generatePrivateKey();
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
return ValidatorClient.new(
|
|
29
|
+
return ValidatorClient.new(
|
|
30
|
+
config,
|
|
31
|
+
deps.epochCache,
|
|
32
|
+
deps.p2pClient,
|
|
33
|
+
deps.blockSource,
|
|
34
|
+
deps.dateProvider,
|
|
35
|
+
deps.telemetry,
|
|
36
|
+
);
|
|
28
37
|
}
|
package/src/validator.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
1
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
3
|
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
3
4
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
-
import
|
|
5
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
7
8
|
import { sleep } from '@aztec/foundation/sleep';
|
|
8
9
|
import { DateProvider, type Timer } from '@aztec/foundation/timer';
|
|
9
10
|
import type { P2P } from '@aztec/p2p';
|
|
10
11
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
11
|
-
import type { L2Block } from '@aztec/stdlib/block';
|
|
12
|
-
import type { BlockAttestation, BlockProposal } from '@aztec/stdlib/p2p';
|
|
13
|
-
import type {
|
|
12
|
+
import type { L2Block, L2BlockSource } from '@aztec/stdlib/block';
|
|
13
|
+
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
14
|
+
import type { ProposedBlockHeader, StateReference, Tx, TxHash } from '@aztec/stdlib/tx';
|
|
14
15
|
import { type TelemetryClient, WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
15
16
|
|
|
16
17
|
import type { ValidatorClientConfig } from './config.js';
|
|
@@ -34,8 +35,9 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
34
35
|
* We reuse the sequencer's block building functionality for re-execution
|
|
35
36
|
*/
|
|
36
37
|
type BlockBuilderCallback = (
|
|
38
|
+
blockNumber: Fr,
|
|
39
|
+
header: ProposedBlockHeader,
|
|
37
40
|
txs: Iterable<Tx> | AsyncIterableIterator<Tx>,
|
|
38
|
-
globalVariables: GlobalVariables,
|
|
39
41
|
opts?: { validateOnly?: boolean },
|
|
40
42
|
) => Promise<{
|
|
41
43
|
block: L2Block;
|
|
@@ -51,10 +53,17 @@ export interface Validator {
|
|
|
51
53
|
registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
|
|
52
54
|
|
|
53
55
|
// Block validation responsibilities
|
|
54
|
-
createBlockProposal(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
createBlockProposal(
|
|
57
|
+
blockNumber: Fr,
|
|
58
|
+
header: ProposedBlockHeader,
|
|
59
|
+
archive: Fr,
|
|
60
|
+
stateReference: StateReference,
|
|
61
|
+
txs: Tx[],
|
|
62
|
+
options: BlockProposalOptions,
|
|
63
|
+
): Promise<BlockProposal | undefined>;
|
|
64
|
+
attestToProposal(proposal: BlockProposal): Promise<BlockAttestation | undefined>;
|
|
65
|
+
|
|
66
|
+
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
58
67
|
collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -81,6 +90,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
81
90
|
private keyStore: ValidatorKeyStore,
|
|
82
91
|
private epochCache: EpochCache,
|
|
83
92
|
private p2pClient: P2P,
|
|
93
|
+
private blockSource: L2BlockSource,
|
|
84
94
|
private config: ValidatorClientConfig,
|
|
85
95
|
private dateProvider: DateProvider = new DateProvider(),
|
|
86
96
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
@@ -94,14 +104,14 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
94
104
|
|
|
95
105
|
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
96
106
|
|
|
97
|
-
// Refresh epoch cache every second to trigger alert if participation in
|
|
107
|
+
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
98
108
|
this.myAddress = this.keyStore.getAddress();
|
|
99
|
-
this.epochCacheUpdateLoop = new RunningPromise(this.
|
|
109
|
+
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
100
110
|
|
|
101
111
|
this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
private async
|
|
114
|
+
private async handleEpochCommitteeUpdate() {
|
|
105
115
|
try {
|
|
106
116
|
const { committee, epoch } = await this.epochCache.getCommittee('now');
|
|
107
117
|
if (epoch !== this.lastEpoch) {
|
|
@@ -122,6 +132,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
122
132
|
config: ValidatorClientConfig,
|
|
123
133
|
epochCache: EpochCache,
|
|
124
134
|
p2pClient: P2P,
|
|
135
|
+
blockSource: L2BlockSource,
|
|
125
136
|
dateProvider: DateProvider = new DateProvider(),
|
|
126
137
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
127
138
|
) {
|
|
@@ -132,7 +143,15 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
132
143
|
const privateKey = validatePrivateKey(config.validatorPrivateKey);
|
|
133
144
|
const localKeyStore = new LocalKeyStore(privateKey);
|
|
134
145
|
|
|
135
|
-
const validator = new ValidatorClient(
|
|
146
|
+
const validator = new ValidatorClient(
|
|
147
|
+
localKeyStore,
|
|
148
|
+
epochCache,
|
|
149
|
+
p2pClient,
|
|
150
|
+
blockSource,
|
|
151
|
+
config,
|
|
152
|
+
dateProvider,
|
|
153
|
+
telemetry,
|
|
154
|
+
);
|
|
136
155
|
validator.registerBlockProposalHandler();
|
|
137
156
|
return validator;
|
|
138
157
|
}
|
|
@@ -178,9 +197,10 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
178
197
|
|
|
179
198
|
async attestToProposal(proposal: BlockProposal): Promise<BlockAttestation | undefined> {
|
|
180
199
|
const slotNumber = proposal.slotNumber.toNumber();
|
|
200
|
+
const blockNumber = proposal.blockNumber.toNumber();
|
|
181
201
|
const proposalInfo = {
|
|
182
202
|
slotNumber,
|
|
183
|
-
blockNumber
|
|
203
|
+
blockNumber,
|
|
184
204
|
archive: proposal.payload.archive.toString(),
|
|
185
205
|
txCount: proposal.payload.txHashes.length,
|
|
186
206
|
txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
|
|
@@ -201,21 +221,40 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
201
221
|
return undefined;
|
|
202
222
|
}
|
|
203
223
|
|
|
224
|
+
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
225
|
+
// Q: Should we move this to the block proposal validator? If there, then p2p would check it
|
|
226
|
+
// before re-broadcasting it. This means that proposals built on top of an L1-reorgd-out block
|
|
227
|
+
// would not be rebroadcasted. But it also means that nodes that have not fully synced would
|
|
228
|
+
// not rebroadcast the proposal.
|
|
229
|
+
if (blockNumber > INITIAL_L2_BLOCK_NUM) {
|
|
230
|
+
const parentBlock = await this.blockSource.getBlock(blockNumber - 1);
|
|
231
|
+
if (parentBlock === undefined) {
|
|
232
|
+
this.log.verbose(`Parent block for ${blockNumber} not found, skipping attestation`);
|
|
233
|
+
this.metrics.incFailedAttestations('parent_block_not_found');
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
237
|
+
this.log.verbose(`Parent block archive root for proposal does not match, skipping attestation`, {
|
|
238
|
+
proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
|
|
239
|
+
parentBlockArchiveRoot: parentBlock.archive.root.toString(),
|
|
240
|
+
...proposalInfo,
|
|
241
|
+
});
|
|
242
|
+
this.metrics.incFailedAttestations('parent_block_does_not_match');
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
204
247
|
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
205
248
|
this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
|
|
206
249
|
try {
|
|
207
|
-
await this.ensureTransactionsAreAvailable(proposal);
|
|
250
|
+
const txs = await this.ensureTransactionsAreAvailable(proposal);
|
|
208
251
|
|
|
209
252
|
if (this.config.validatorReexecute) {
|
|
210
253
|
this.log.verbose(`Re-executing transactions in the proposal before attesting`);
|
|
211
|
-
await this.reExecuteTransactions(proposal);
|
|
254
|
+
await this.reExecuteTransactions(proposal, txs);
|
|
212
255
|
}
|
|
213
256
|
} catch (error: any) {
|
|
214
|
-
|
|
215
|
-
this.metrics.incFailedAttestations(error.name);
|
|
216
|
-
} else {
|
|
217
|
-
this.metrics.incFailedAttestations('unknown');
|
|
218
|
-
}
|
|
257
|
+
this.metrics.incFailedAttestations(error instanceof Error ? error.name : 'unknown');
|
|
219
258
|
|
|
220
259
|
// If the transactions are not available, then we should not attempt to attest
|
|
221
260
|
if (error instanceof TransactionsNotAvailableError) {
|
|
@@ -240,16 +279,14 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
240
279
|
* Re-execute the transactions in the proposal and check that the state updates match the header state
|
|
241
280
|
* @param proposal - The proposal to re-execute
|
|
242
281
|
*/
|
|
243
|
-
async reExecuteTransactions(proposal: BlockProposal) {
|
|
282
|
+
async reExecuteTransactions(proposal: BlockProposal, txs: Tx[]) {
|
|
244
283
|
const { header, txHashes } = proposal.payload;
|
|
245
284
|
|
|
246
|
-
|
|
247
|
-
tx => tx !== undefined,
|
|
248
|
-
) as Tx[];
|
|
249
|
-
|
|
250
|
-
// If we cannot request all of the transactions, then we should fail
|
|
285
|
+
// If we do not have all of the transactions, then we should fail
|
|
251
286
|
if (txs.length !== txHashes.length) {
|
|
252
|
-
|
|
287
|
+
const foundTxHashes = await Promise.all(txs.map(async tx => await tx.getTxHash()));
|
|
288
|
+
const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
|
|
289
|
+
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
253
290
|
}
|
|
254
291
|
|
|
255
292
|
// Assertion: This check will fail if re-execution is not enabled
|
|
@@ -259,7 +296,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
259
296
|
|
|
260
297
|
// Use the sequencer's block building logic to re-execute the transactions
|
|
261
298
|
const stopTimer = this.metrics.reExecutionTimer();
|
|
262
|
-
const { block, numFailedTxs } = await this.blockBuilder(
|
|
299
|
+
const { block, numFailedTxs } = await this.blockBuilder(proposal.blockNumber, header, txs, {
|
|
263
300
|
validateOnly: true,
|
|
264
301
|
});
|
|
265
302
|
stopTimer();
|
|
@@ -291,43 +328,130 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
291
328
|
* 3. If we cannot retrieve them from the network, throw an error
|
|
292
329
|
* @param proposal - The proposal to attest to
|
|
293
330
|
*/
|
|
294
|
-
async ensureTransactionsAreAvailable(proposal: BlockProposal) {
|
|
295
|
-
|
|
296
|
-
|
|
331
|
+
async ensureTransactionsAreAvailable(proposal: BlockProposal): Promise<Tx[]> {
|
|
332
|
+
if (proposal.payload.txHashes.length === 0) {
|
|
333
|
+
this.log.verbose(`Received block proposal with no transactions, skipping transaction availability check`);
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
// Is this a new style proposal?
|
|
337
|
+
if (proposal.txs && proposal.txs.length > 0 && proposal.txs.length === proposal.payload.txHashes.length) {
|
|
338
|
+
// Yes, any txs that we already have we should use
|
|
339
|
+
this.log.info(`Using new style proposal with ${proposal.txs.length} transactions`);
|
|
340
|
+
|
|
341
|
+
// Request from the pool based on the signed hashes in the payload
|
|
342
|
+
const hashesFromPayload = proposal.payload.txHashes;
|
|
343
|
+
const txsToUse = await this.p2pClient.getTxsByHashFromPool(hashesFromPayload);
|
|
344
|
+
|
|
345
|
+
const missingTxs = txsToUse.filter(tx => tx === undefined).length;
|
|
346
|
+
if (missingTxs > 0) {
|
|
347
|
+
this.log.verbose(
|
|
348
|
+
`Missing ${missingTxs}/${hashesFromPayload.length} transactions in the tx pool, will attempt to take from the proposal`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
297
351
|
|
|
298
|
-
|
|
352
|
+
let usedFromProposal = 0;
|
|
353
|
+
|
|
354
|
+
// Fill any holes with txs in the proposal, provided their hash matches the hash in the payload
|
|
355
|
+
for (let i = 0; i < txsToUse.length; i++) {
|
|
356
|
+
if (txsToUse[i] === undefined) {
|
|
357
|
+
// We don't have the transaction, take from the proposal, provided the hash is the same
|
|
358
|
+
const hashOfTxInProposal = await proposal.txs[i].getTxHash();
|
|
359
|
+
if (hashOfTxInProposal.equals(hashesFromPayload[i])) {
|
|
360
|
+
// Hash is equal, we can use the tx from the proposal
|
|
361
|
+
txsToUse[i] = proposal.txs[i];
|
|
362
|
+
usedFromProposal++;
|
|
363
|
+
} else {
|
|
364
|
+
this.log.warn(
|
|
365
|
+
`Unable to take tx: ${hashOfTxInProposal.toString()} from the proposal, it does not match payload hash: ${hashesFromPayload[
|
|
366
|
+
i
|
|
367
|
+
].toString()}`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
299
372
|
|
|
300
|
-
|
|
301
|
-
|
|
373
|
+
// See if we still have any holes, if there are then we were not successful and will try the old method
|
|
374
|
+
if (txsToUse.some(tx => tx === undefined)) {
|
|
375
|
+
this.log.warn(`Failed to use transactions from proposal. Falling back to old proposal logic`);
|
|
376
|
+
} else {
|
|
377
|
+
this.log.info(
|
|
378
|
+
`Successfully used ${usedFromProposal}/${hashesFromPayload.length} transactions from the proposal`,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
await this.p2pClient.validate(txsToUse as Tx[]);
|
|
382
|
+
return txsToUse as Tx[];
|
|
383
|
+
}
|
|
302
384
|
}
|
|
303
385
|
|
|
304
|
-
this.log.
|
|
386
|
+
this.log.info(`Using old style proposal with ${proposal.payload.txHashes.length} transactions`);
|
|
387
|
+
|
|
388
|
+
// Old style proposal, we will perform a request by hash from pool
|
|
389
|
+
// This will request from network any txs that are missing
|
|
390
|
+
const txHashes: TxHash[] = proposal.payload.txHashes;
|
|
391
|
+
|
|
392
|
+
// This part is just for logging that we are requesting from the network
|
|
393
|
+
const availability = await this.p2pClient.hasTxsInPool(txHashes);
|
|
394
|
+
const notAvailable = availability.filter(availability => availability === false);
|
|
395
|
+
if (notAvailable.length) {
|
|
396
|
+
this.log.verbose(
|
|
397
|
+
`Missing ${notAvailable.length} transactions in the tx pool, will need to request from the network`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
305
400
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
401
|
+
// This will request from the network any txs that are missing
|
|
402
|
+
const retrievedTxs = await this.p2pClient.getTxsByHash(txHashes);
|
|
403
|
+
const missingTxs = retrievedTxs
|
|
404
|
+
.map((tx, index) => {
|
|
405
|
+
// Return the hash of any that we did not get
|
|
406
|
+
if (tx === undefined) {
|
|
407
|
+
return txHashes[index];
|
|
408
|
+
} else {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
.filter(hash => hash !== undefined);
|
|
413
|
+
if (missingTxs.length > 0) {
|
|
414
|
+
throw new TransactionsNotAvailableError(missingTxs as TxHash[]);
|
|
309
415
|
}
|
|
416
|
+
|
|
417
|
+
await this.p2pClient.validate(retrievedTxs as Tx[]);
|
|
418
|
+
|
|
419
|
+
return retrievedTxs as Tx[];
|
|
310
420
|
}
|
|
311
421
|
|
|
312
|
-
async createBlockProposal(
|
|
313
|
-
|
|
422
|
+
async createBlockProposal(
|
|
423
|
+
blockNumber: Fr,
|
|
424
|
+
header: ProposedBlockHeader,
|
|
425
|
+
archive: Fr,
|
|
426
|
+
stateReference: StateReference,
|
|
427
|
+
txs: Tx[],
|
|
428
|
+
options: BlockProposalOptions,
|
|
429
|
+
): Promise<BlockProposal | undefined> {
|
|
430
|
+
if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
|
|
314
431
|
this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
315
432
|
return Promise.resolve(undefined);
|
|
316
433
|
}
|
|
317
434
|
|
|
318
|
-
const newProposal = await this.validationService.createBlockProposal(
|
|
435
|
+
const newProposal = await this.validationService.createBlockProposal(
|
|
436
|
+
blockNumber,
|
|
437
|
+
header,
|
|
438
|
+
archive,
|
|
439
|
+
stateReference,
|
|
440
|
+
txs,
|
|
441
|
+
options,
|
|
442
|
+
);
|
|
319
443
|
this.previousProposal = newProposal;
|
|
320
444
|
return newProposal;
|
|
321
445
|
}
|
|
322
446
|
|
|
323
|
-
broadcastBlockProposal(proposal: BlockProposal): void {
|
|
324
|
-
this.p2pClient.broadcastProposal(proposal);
|
|
447
|
+
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
448
|
+
await this.p2pClient.broadcastProposal(proposal);
|
|
325
449
|
}
|
|
326
450
|
|
|
327
451
|
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
|
|
328
452
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
329
453
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
330
|
-
const slot = proposal.payload.header.
|
|
454
|
+
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
331
455
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
332
456
|
|
|
333
457
|
if (+deadline < this.dateProvider.now()) {
|
|
@@ -378,7 +502,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
378
502
|
function validatePrivateKey(privateKey: string): Buffer32 {
|
|
379
503
|
try {
|
|
380
504
|
return Buffer32.fromString(privateKey);
|
|
381
|
-
} catch
|
|
505
|
+
} catch {
|
|
382
506
|
throw new InvalidValidatorPrivateKeyError();
|
|
383
507
|
}
|
|
384
508
|
}
|