@aztec/validator-client 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.1
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/block_proposal_handler.d.ts +53 -0
- package/dest/block_proposal_handler.d.ts.map +1 -0
- package/dest/block_proposal_handler.js +290 -0
- package/dest/config.d.ts +4 -20
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +18 -2
- package/dest/duties/validation_service.d.ts +11 -6
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +19 -7
- package/dest/factory.d.ts +14 -5
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +10 -0
- package/dest/index.d.ts +2 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -0
- package/dest/key_store/index.d.ts +1 -1
- package/dest/key_store/interface.d.ts +1 -1
- package/dest/key_store/local_key_store.d.ts +1 -1
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +1 -1
- package/dest/key_store/node_keystore_adapter.d.ts +1 -1
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +2 -4
- package/dest/key_store/web3signer_key_store.d.ts +1 -7
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +7 -8
- package/dest/metrics.d.ts +7 -5
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +25 -12
- package/dest/validator.d.ts +28 -38
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +182 -205
- package/package.json +15 -15
- package/src/block_proposal_handler.ts +346 -0
- package/src/config.ts +30 -25
- package/src/duties/validation_service.ts +30 -12
- package/src/factory.ts +34 -4
- package/src/index.ts +1 -0
- package/src/key_store/local_key_store.ts +1 -1
- package/src/key_store/node_keystore_adapter.ts +3 -4
- package/src/key_store/web3signer_key_store.ts +7 -10
- package/src/metrics.ts +34 -13
- package/src/validator.ts +252 -299
package/dest/validator.js
CHANGED
|
@@ -1,68 +1,67 @@
|
|
|
1
|
-
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
|
-
import { Fr } from '@aztec/foundation/fields';
|
|
3
1
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
5
2
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
6
3
|
import { sleep } from '@aztec/foundation/sleep';
|
|
7
|
-
import { DateProvider
|
|
8
|
-
import { AuthRequest, AuthResponse, ReqRespSubProtocol } from '@aztec/p2p';
|
|
9
|
-
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
10
|
-
import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
|
|
4
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
|
+
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
11
6
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
12
|
-
import {
|
|
13
|
-
import { GlobalVariables } from '@aztec/stdlib/tx';
|
|
14
|
-
import { AttestationTimeoutError, ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
|
|
7
|
+
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
15
8
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
16
9
|
import { EventEmitter } from 'events';
|
|
10
|
+
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
17
11
|
import { ValidationService } from './duties/validation_service.js';
|
|
18
12
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
19
13
|
import { ValidatorMetrics } from './metrics.js';
|
|
20
14
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
21
15
|
// Just cap the set to avoid unbounded growth.
|
|
22
16
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
17
|
+
// What errors from the block proposal handler result in slashing
|
|
18
|
+
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
19
|
+
'state_mismatch',
|
|
20
|
+
'failed_txs'
|
|
21
|
+
];
|
|
23
22
|
/**
|
|
24
23
|
* Validator Client
|
|
25
24
|
*/ export class ValidatorClient extends EventEmitter {
|
|
26
|
-
blockBuilder;
|
|
27
25
|
keyStore;
|
|
28
26
|
epochCache;
|
|
29
27
|
p2pClient;
|
|
30
|
-
|
|
31
|
-
l1ToL2MessageSource;
|
|
32
|
-
txProvider;
|
|
28
|
+
blockProposalHandler;
|
|
33
29
|
config;
|
|
34
30
|
dateProvider;
|
|
35
|
-
log;
|
|
36
31
|
tracer;
|
|
37
32
|
validationService;
|
|
38
33
|
metrics;
|
|
34
|
+
log;
|
|
35
|
+
// Whether it has already registered handlers on the p2p client
|
|
36
|
+
hasRegisteredHandlers;
|
|
39
37
|
// Used to check if we are sending the same proposal twice
|
|
40
38
|
previousProposal;
|
|
41
|
-
myAddresses;
|
|
42
39
|
lastEpochForCommitteeUpdateLoop;
|
|
43
40
|
epochCacheUpdateLoop;
|
|
44
|
-
blockProposalValidator;
|
|
45
41
|
proposersOfInvalidBlocks;
|
|
46
|
-
constructor(
|
|
47
|
-
super(), this.
|
|
42
|
+
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
43
|
+
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.config = config, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
|
|
44
|
+
// Create child logger with fisherman prefix if in fisherman mode
|
|
45
|
+
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
48
46
|
this.tracer = telemetry.getTracer('Validator');
|
|
49
47
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
50
|
-
this.validationService = new ValidationService(keyStore);
|
|
51
|
-
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
48
|
+
this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
|
|
52
49
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
this.log.verbose(`Initialized validator with addresses: ${
|
|
50
|
+
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
51
|
+
const myAddresses = this.getValidatorAddresses();
|
|
52
|
+
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
|
|
56
53
|
}
|
|
57
|
-
static validateKeyStoreConfiguration(keyStoreManager) {
|
|
54
|
+
static validateKeyStoreConfiguration(keyStoreManager, logger) {
|
|
58
55
|
const validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
59
56
|
const validatorAddresses = validatorKeyStore.getAddresses();
|
|
60
57
|
// Verify that we can retrieve all required data from the key store
|
|
61
58
|
for (const address of validatorAddresses){
|
|
62
59
|
// Functions throw if required data is not available
|
|
60
|
+
let coinbase;
|
|
61
|
+
let feeRecipient;
|
|
63
62
|
try {
|
|
64
|
-
validatorKeyStore.getCoinbaseAddress(address);
|
|
65
|
-
validatorKeyStore.getFeeRecipient(address);
|
|
63
|
+
coinbase = validatorKeyStore.getCoinbaseAddress(address);
|
|
64
|
+
feeRecipient = validatorKeyStore.getFeeRecipient(address);
|
|
66
65
|
} catch (error) {
|
|
67
66
|
throw new Error(`Failed to retrieve required data for validator address ${address}, error: ${error}`);
|
|
68
67
|
}
|
|
@@ -70,6 +69,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
70
69
|
if (!publisherAddresses.length) {
|
|
71
70
|
throw new Error(`No publisher addresses found for validator address ${address}`);
|
|
72
71
|
}
|
|
72
|
+
logger?.debug(`Validator ${address.toString()} configured with coinbase ${coinbase.toString()}, feeRecipient ${feeRecipient.toString()} and publishers ${publisherAddresses.map((x)=>x.toString()).join()}`);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
async handleEpochCommitteeUpdate() {
|
|
@@ -80,7 +80,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
83
|
-
const me = this.
|
|
83
|
+
const me = this.getValidatorAddresses();
|
|
84
84
|
const committeeSet = new Set(committee.map((v)=>v.toString()));
|
|
85
85
|
const inCommittee = me.filter((a)=>committeeSet.has(a.toString()));
|
|
86
86
|
if (inCommittee.length > 0) {
|
|
@@ -95,13 +95,23 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
static new(config, blockBuilder, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
const metrics = new ValidatorMetrics(telemetry);
|
|
99
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
100
|
+
txsPermitted: !config.disableTransactions
|
|
101
|
+
});
|
|
102
|
+
const blockProposalHandler = new BlockProposalHandler(blockBuilder, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
|
|
103
|
+
const validator = new ValidatorClient(NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockProposalHandler, config, dateProvider, telemetry);
|
|
101
104
|
return validator;
|
|
102
105
|
}
|
|
103
106
|
getValidatorAddresses() {
|
|
104
|
-
return this.keyStore.getAddresses();
|
|
107
|
+
return this.keyStore.getAddresses().filter((addr)=>!this.config.disabledValidators.some((disabled)=>disabled.equals(addr)));
|
|
108
|
+
}
|
|
109
|
+
getBlockProposalHandler() {
|
|
110
|
+
return this.blockProposalHandler;
|
|
111
|
+
}
|
|
112
|
+
// Proxy method for backwards compatibility with tests
|
|
113
|
+
reExecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages) {
|
|
114
|
+
return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages);
|
|
105
115
|
}
|
|
106
116
|
signWithAddress(addr, msg) {
|
|
107
117
|
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
@@ -112,37 +122,54 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
112
122
|
getFeeRecipientForAttestor(attestor) {
|
|
113
123
|
return this.keyStore.getFeeRecipient(attestor);
|
|
114
124
|
}
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
getConfig() {
|
|
126
|
+
return this.config;
|
|
127
|
+
}
|
|
128
|
+
updateConfig(config) {
|
|
129
|
+
this.config = {
|
|
130
|
+
...this.config,
|
|
131
|
+
...config
|
|
132
|
+
};
|
|
117
133
|
}
|
|
118
134
|
async start() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
136
|
+
this.log.warn(`Validator client already started`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await this.registerHandlers();
|
|
140
|
+
const myAddresses = this.getValidatorAddresses();
|
|
122
141
|
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
142
|
+
this.log.info(`Started validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
|
|
123
143
|
if (inCommittee.length > 0) {
|
|
124
|
-
this.log.info(`
|
|
125
|
-
} else {
|
|
126
|
-
this.log.info(`Started validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
|
|
144
|
+
this.log.info(`Addresses in current validator committee: ${inCommittee.map((a)=>a.toString()).join(', ')}`);
|
|
127
145
|
}
|
|
128
146
|
this.epochCacheUpdateLoop.start();
|
|
129
|
-
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
130
|
-
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
131
147
|
return Promise.resolve();
|
|
132
148
|
}
|
|
133
149
|
async stop() {
|
|
134
150
|
await this.epochCacheUpdateLoop.stop();
|
|
135
151
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
/** Register handlers on the p2p client */ async registerHandlers() {
|
|
153
|
+
if (!this.hasRegisteredHandlers) {
|
|
154
|
+
this.hasRegisteredHandlers = true;
|
|
155
|
+
this.log.debug(`Registering validator handlers for p2p client`);
|
|
156
|
+
const handler = (block, proposalSender)=>this.attestToProposal(block, proposalSender);
|
|
157
|
+
this.p2pClient.registerBlockProposalHandler(handler);
|
|
158
|
+
const myAddresses = this.getValidatorAddresses();
|
|
159
|
+
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
160
|
+
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
161
|
+
}
|
|
139
162
|
}
|
|
140
163
|
async attestToProposal(proposal, proposalSender) {
|
|
141
|
-
const slotNumber = proposal.slotNumber
|
|
142
|
-
const blockNumber = proposal.blockNumber;
|
|
164
|
+
const slotNumber = proposal.slotNumber;
|
|
143
165
|
const proposer = proposal.getSender();
|
|
166
|
+
// Reject proposals with invalid signatures
|
|
167
|
+
if (!proposer) {
|
|
168
|
+
this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
144
171
|
// Check that I have any address in current committee before attesting
|
|
145
|
-
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.
|
|
172
|
+
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
146
173
|
const partOfCommittee = inCommittee.length > 0;
|
|
147
174
|
const proposalInfo = {
|
|
148
175
|
...proposal.toBlockInfo(),
|
|
@@ -150,164 +177,84 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
150
177
|
};
|
|
151
178
|
this.log.info(`Received proposal for slot ${slotNumber}`, {
|
|
152
179
|
...proposalInfo,
|
|
153
|
-
txHashes: proposal.txHashes.map((
|
|
180
|
+
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
181
|
+
fishermanMode: this.config.fishermanMode || false
|
|
154
182
|
});
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Q: Should we move this to the block proposal validator? If there, then p2p would check it
|
|
178
|
-
// before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
|
|
179
|
-
// would not be rebroadcasted. But it also means that nodes that have not fully synced would
|
|
180
|
-
// not rebroadcast the proposal.
|
|
181
|
-
if (blockNumber > INITIAL_L2_BLOCK_NUM) {
|
|
182
|
-
const config = this.blockBuilder.getConfig();
|
|
183
|
-
const deadline = this.getReexecutionDeadline(proposal, config);
|
|
184
|
-
const currentTime = this.dateProvider.now();
|
|
185
|
-
const timeoutDurationMs = deadline.getTime() - currentTime;
|
|
186
|
-
const parentBlock = timeoutDurationMs <= 0 ? undefined : await retryUntil(async ()=>{
|
|
187
|
-
const block = await this.blockSource.getBlock(blockNumber - 1);
|
|
188
|
-
if (block) {
|
|
189
|
-
return block;
|
|
190
|
-
}
|
|
191
|
-
await this.blockSource.syncImmediate();
|
|
192
|
-
return await this.blockSource.getBlock(blockNumber - 1);
|
|
193
|
-
}, 'Force Archiver Sync', timeoutDurationMs / 1000, 0.5);
|
|
194
|
-
if (parentBlock === undefined) {
|
|
195
|
-
this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
|
|
196
|
-
if (partOfCommittee) {
|
|
197
|
-
this.metrics.incFailedAttestations(1, 'parent_block_not_found');
|
|
198
|
-
}
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
202
|
-
this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
|
|
203
|
-
proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
|
|
204
|
-
parentBlockArchiveRoot: parentBlock.archive.root.toString(),
|
|
205
|
-
...proposalInfo
|
|
206
|
-
});
|
|
207
|
-
if (partOfCommittee) {
|
|
208
|
-
this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
|
|
209
|
-
}
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
214
|
-
// Q: Same as above, should this be part of p2p validation?
|
|
215
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
|
|
216
|
-
const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
217
|
-
const proposalInHash = proposal.payload.header.contentCommitment.inHash;
|
|
218
|
-
if (!computedInHash.equals(proposalInHash)) {
|
|
219
|
-
this.log.warn(`L1 to L2 messages in hash mismatch, skipping attestation`, {
|
|
220
|
-
proposalInHash: proposalInHash.toString(),
|
|
221
|
-
computedInHash: computedInHash.toString(),
|
|
222
|
-
...proposalInfo
|
|
223
|
-
});
|
|
224
|
-
if (partOfCommittee) {
|
|
225
|
-
this.metrics.incFailedAttestations(1, 'in_hash_mismatch');
|
|
226
|
-
}
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
230
|
-
if (missingTxs.length > 0) {
|
|
231
|
-
this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, {
|
|
232
|
-
...proposalInfo,
|
|
233
|
-
missingTxs
|
|
234
|
-
});
|
|
235
|
-
if (partOfCommittee) {
|
|
236
|
-
this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
|
|
237
|
-
}
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
// Try re-executing the transactions in the proposal
|
|
241
|
-
try {
|
|
242
|
-
this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
|
|
243
|
-
if (this.config.validatorReexecute) {
|
|
244
|
-
this.log.verbose(`Re-executing transactions in the proposal before attesting`);
|
|
245
|
-
await this.reExecuteTransactions(proposal, txs, l1ToL2Messages);
|
|
183
|
+
// Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
|
|
184
|
+
// invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
|
|
185
|
+
// In fisherman mode, we always reexecute to validate proposals.
|
|
186
|
+
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
187
|
+
const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals;
|
|
188
|
+
const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
|
|
189
|
+
if (!validationResult.isValid) {
|
|
190
|
+
this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
191
|
+
const reason = validationResult.reason || 'unknown';
|
|
192
|
+
// Classify failure reason: bad proposal vs node issue
|
|
193
|
+
const badProposalReasons = [
|
|
194
|
+
'invalid_proposal',
|
|
195
|
+
'state_mismatch',
|
|
196
|
+
'failed_txs',
|
|
197
|
+
'in_hash_mismatch',
|
|
198
|
+
'parent_block_wrong_slot'
|
|
199
|
+
];
|
|
200
|
+
if (badProposalReasons.includes(reason)) {
|
|
201
|
+
this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
|
|
202
|
+
} else {
|
|
203
|
+
// Node issues so we can't attest
|
|
204
|
+
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
246
205
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
250
|
-
if (error instanceof ReExStateMismatchError && this.config.slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
206
|
+
// Slash invalid block proposals (can happen even when not in committee)
|
|
207
|
+
if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
251
208
|
this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
|
|
252
209
|
this.slashInvalidBlock(proposal);
|
|
253
210
|
}
|
|
254
211
|
return undefined;
|
|
255
212
|
}
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
getReexecutionDeadline(proposal, config) {
|
|
263
|
-
const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
|
|
264
|
-
const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
|
|
265
|
-
return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Re-execute the transactions in the proposal and check that the state updates match the header state
|
|
269
|
-
* @param proposal - The proposal to re-execute
|
|
270
|
-
*/ async reExecuteTransactions(proposal, txs, l1ToL2Messages) {
|
|
271
|
-
const { header } = proposal.payload;
|
|
272
|
-
const { txHashes } = proposal;
|
|
273
|
-
// If we do not have all of the transactions, then we should fail
|
|
274
|
-
if (txs.length !== txHashes.length) {
|
|
275
|
-
const foundTxHashes = txs.map((tx)=>tx.getTxHash());
|
|
276
|
-
const missingTxHashes = txHashes.filter((txHash)=>!foundTxHashes.includes(txHash));
|
|
277
|
-
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
213
|
+
// Check that I have any address in current committee before attesting
|
|
214
|
+
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
215
|
+
if (!partOfCommittee && !this.config.fishermanMode) {
|
|
216
|
+
this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
|
|
217
|
+
return undefined;
|
|
278
218
|
}
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
blockNumber: proposal.blockNumber,
|
|
285
|
-
timestamp: header.timestamp,
|
|
286
|
-
chainId: new Fr(config.l1ChainId),
|
|
287
|
-
version: new Fr(config.rollupVersion)
|
|
288
|
-
});
|
|
289
|
-
const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, globalVariables, {
|
|
290
|
-
deadline: this.getReexecutionDeadline(proposal, config)
|
|
219
|
+
// Provided all of the above checks pass, we can attest to the proposal
|
|
220
|
+
this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} proposal for slot ${slotNumber}`, {
|
|
221
|
+
...proposalInfo,
|
|
222
|
+
inCommittee: partOfCommittee,
|
|
223
|
+
fishermanMode: this.config.fishermanMode || false
|
|
291
224
|
});
|
|
292
|
-
this.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
225
|
+
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
226
|
+
// If the above function does not throw an error, then we can attest to the proposal
|
|
227
|
+
// Determine which validators should attest
|
|
228
|
+
let attestors;
|
|
229
|
+
if (partOfCommittee) {
|
|
230
|
+
attestors = inCommittee;
|
|
231
|
+
} else if (this.config.fishermanMode) {
|
|
232
|
+
// In fisherman mode, create attestations for validation purposes even if not in committee. These won't be broadcast.
|
|
233
|
+
attestors = this.getValidatorAddresses();
|
|
234
|
+
} else {
|
|
235
|
+
attestors = [];
|
|
297
236
|
}
|
|
298
|
-
if
|
|
299
|
-
|
|
300
|
-
|
|
237
|
+
// Only create attestations if we have attestors
|
|
238
|
+
if (attestors.length === 0) {
|
|
239
|
+
return undefined;
|
|
301
240
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.
|
|
305
|
-
|
|
241
|
+
if (this.config.fishermanMode) {
|
|
242
|
+
// bail out early and don't save attestations to the pool in fisherman mode
|
|
243
|
+
this.log.info(`Creating attestations for proposal for slot ${slotNumber}`, {
|
|
244
|
+
...proposalInfo,
|
|
245
|
+
attestors: attestors.map((a)=>a.toString())
|
|
246
|
+
});
|
|
247
|
+
return undefined;
|
|
306
248
|
}
|
|
307
|
-
this.
|
|
249
|
+
return this.createBlockAttestationsFromProposal(proposal, attestors);
|
|
308
250
|
}
|
|
309
251
|
slashInvalidBlock(proposal) {
|
|
310
252
|
const proposer = proposal.getSender();
|
|
253
|
+
// Skip if signature is invalid (shouldn't happen since we validate earlier)
|
|
254
|
+
if (!proposer) {
|
|
255
|
+
this.log.warn(`Cannot slash proposal with invalid signature`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
311
258
|
// Trim the set if it's too big.
|
|
312
259
|
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
313
260
|
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
@@ -319,33 +266,46 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
319
266
|
validator: proposer,
|
|
320
267
|
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
321
268
|
offenseType: OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL,
|
|
322
|
-
epochOrSlot: proposal.slotNumber
|
|
269
|
+
epochOrSlot: BigInt(proposal.slotNumber)
|
|
323
270
|
}
|
|
324
271
|
]);
|
|
325
272
|
}
|
|
326
|
-
async createBlockProposal(blockNumber, header, archive,
|
|
327
|
-
if (this.previousProposal?.slotNumber
|
|
273
|
+
async createBlockProposal(blockNumber, header, archive, txs, proposerAddress, options) {
|
|
274
|
+
if (this.previousProposal?.slotNumber === header.slotNumber) {
|
|
328
275
|
this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
329
276
|
return Promise.resolve(undefined);
|
|
330
277
|
}
|
|
331
|
-
const newProposal = await this.validationService.createBlockProposal(
|
|
278
|
+
const newProposal = await this.validationService.createBlockProposal(header, archive, txs, proposerAddress, {
|
|
279
|
+
...options,
|
|
280
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
281
|
+
});
|
|
332
282
|
this.previousProposal = newProposal;
|
|
333
283
|
return newProposal;
|
|
334
284
|
}
|
|
335
285
|
async broadcastBlockProposal(proposal) {
|
|
336
286
|
await this.p2pClient.broadcastProposal(proposal);
|
|
337
287
|
}
|
|
288
|
+
async signAttestationsAndSigners(attestationsAndSigners, proposer) {
|
|
289
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
290
|
+
}
|
|
338
291
|
async collectOwnAttestations(proposal) {
|
|
339
|
-
const slot = proposal.payload.header.slotNumber
|
|
340
|
-
const inCommittee = await this.epochCache.filterInCommittee(slot, this.
|
|
292
|
+
const slot = proposal.payload.header.slotNumber;
|
|
293
|
+
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
341
294
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
|
|
342
295
|
inCommittee
|
|
343
296
|
});
|
|
344
|
-
|
|
297
|
+
const attestations = await this.createBlockAttestationsFromProposal(proposal, inCommittee);
|
|
298
|
+
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
299
|
+
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
300
|
+
// due to inactivity for missed attestations.
|
|
301
|
+
void this.p2pClient.broadcastAttestations(attestations).catch((err)=>{
|
|
302
|
+
this.log.error(`Failed to broadcast self-attestations for slot ${slot}`, err);
|
|
303
|
+
});
|
|
304
|
+
return attestations;
|
|
345
305
|
}
|
|
346
306
|
async collectAttestations(proposal, required, deadline) {
|
|
347
307
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
348
|
-
const slot = proposal.payload.header.slotNumber
|
|
308
|
+
const slot = proposal.payload.header.slotNumber;
|
|
349
309
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
350
310
|
if (+deadline < this.dateProvider.now()) {
|
|
351
311
|
this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
|
|
@@ -353,14 +313,31 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
353
313
|
}
|
|
354
314
|
await this.collectOwnAttestations(proposal);
|
|
355
315
|
const proposalId = proposal.archive.toString();
|
|
356
|
-
const myAddresses = this.
|
|
316
|
+
const myAddresses = this.getValidatorAddresses();
|
|
357
317
|
let attestations = [];
|
|
358
318
|
while(true){
|
|
359
|
-
|
|
319
|
+
// Filter out attestations with a mismatching payload. This should NOT happen since we have verified
|
|
320
|
+
// the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
|
|
321
|
+
const collectedAttestations = (await this.p2pClient.getAttestationsForSlot(slot, proposalId)).filter((attestation)=>{
|
|
322
|
+
if (!attestation.payload.equals(proposal.payload)) {
|
|
323
|
+
this.log.warn(`Received attestation for slot ${slot} with mismatched payload from ${attestation.getSender()?.toString()}`, {
|
|
324
|
+
attestationPayload: attestation.payload,
|
|
325
|
+
proposalPayload: proposal.payload
|
|
326
|
+
});
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
});
|
|
331
|
+
// Log new attestations we collected
|
|
360
332
|
const oldSenders = attestations.map((attestation)=>attestation.getSender());
|
|
361
333
|
for (const collected of collectedAttestations){
|
|
362
334
|
const collectedSender = collected.getSender();
|
|
363
|
-
|
|
335
|
+
// Skip attestations with invalid signatures
|
|
336
|
+
if (!collectedSender) {
|
|
337
|
+
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (!myAddresses.some((address)=>address.equals(collectedSender)) && !oldSenders.some((sender)=>sender?.equals(collectedSender))) {
|
|
364
341
|
this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
|
|
365
342
|
}
|
|
366
343
|
}
|
|
@@ -373,11 +350,11 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
373
350
|
this.log.error(`Timeout ${deadline.toISOString()} waiting for ${required} attestations for slot ${slot}`);
|
|
374
351
|
throw new AttestationTimeoutError(attestations.length, required, slot);
|
|
375
352
|
}
|
|
376
|
-
this.log.debug(`Collected ${attestations.length} attestations so far`);
|
|
353
|
+
this.log.debug(`Collected ${attestations.length} of ${required} attestations so far`);
|
|
377
354
|
await sleep(this.config.attestationPollingIntervalMs);
|
|
378
355
|
}
|
|
379
356
|
}
|
|
380
|
-
async
|
|
357
|
+
async createBlockAttestationsFromProposal(proposal, attestors = []) {
|
|
381
358
|
const attestations = await this.validationService.attestToProposal(proposal, attestors);
|
|
382
359
|
await this.p2pClient.addAttestations(attestations);
|
|
383
360
|
return attestations;
|
|
@@ -400,4 +377,4 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
|
400
377
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
401
378
|
return authResponse.toBuffer();
|
|
402
379
|
}
|
|
403
|
-
}
|
|
380
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "3.0.0-
|
|
3
|
+
"version": "3.0.0-devnet.2-patch.1",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "node --no-warnings ./dest/bin",
|
|
21
|
-
"build": "yarn clean && tsc
|
|
22
|
-
"build:dev": "tsc
|
|
21
|
+
"build": "yarn clean && ../scripts/tsc.sh",
|
|
22
|
+
"build:dev": "../scripts/tsc.sh --watch",
|
|
23
23
|
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
24
24
|
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
25
25
|
},
|
|
@@ -64,25 +64,25 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@aztec/constants": "3.0.0-
|
|
68
|
-
"@aztec/epoch-cache": "3.0.0-
|
|
69
|
-
"@aztec/ethereum": "3.0.0-
|
|
70
|
-
"@aztec/foundation": "3.0.0-
|
|
71
|
-
"@aztec/node-keystore": "3.0.0-
|
|
72
|
-
"@aztec/p2p": "3.0.0-
|
|
73
|
-
"@aztec/
|
|
74
|
-
"@aztec/
|
|
75
|
-
"@aztec/
|
|
76
|
-
"@aztec/telemetry-client": "3.0.0-canary.a9708bd",
|
|
67
|
+
"@aztec/constants": "3.0.0-devnet.2-patch.1",
|
|
68
|
+
"@aztec/epoch-cache": "3.0.0-devnet.2-patch.1",
|
|
69
|
+
"@aztec/ethereum": "3.0.0-devnet.2-patch.1",
|
|
70
|
+
"@aztec/foundation": "3.0.0-devnet.2-patch.1",
|
|
71
|
+
"@aztec/node-keystore": "3.0.0-devnet.2-patch.1",
|
|
72
|
+
"@aztec/p2p": "3.0.0-devnet.2-patch.1",
|
|
73
|
+
"@aztec/slasher": "3.0.0-devnet.2-patch.1",
|
|
74
|
+
"@aztec/stdlib": "3.0.0-devnet.2-patch.1",
|
|
75
|
+
"@aztec/telemetry-client": "3.0.0-devnet.2-patch.1",
|
|
77
76
|
"koa": "^2.16.1",
|
|
78
|
-
"koa-router": "^
|
|
77
|
+
"koa-router": "^13.1.1",
|
|
79
78
|
"tslib": "^2.4.0",
|
|
80
|
-
"viem": "2.
|
|
79
|
+
"viem": "npm:@aztec/viem@2.38.2"
|
|
81
80
|
},
|
|
82
81
|
"devDependencies": {
|
|
83
82
|
"@jest/globals": "^30.0.0",
|
|
84
83
|
"@types/jest": "^30.0.0",
|
|
85
84
|
"@types/node": "^22.15.17",
|
|
85
|
+
"@typescript/native-preview": "7.0.0-dev.20251126.1",
|
|
86
86
|
"jest": "^30.0.0",
|
|
87
87
|
"jest-mock-extended": "^4.0.0",
|
|
88
88
|
"ts-node": "^10.9.1",
|