@aztec/validator-client 0.0.1-commit.033589e → 0.0.1-commit.04852196a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dest/block_proposal_handler.d.ts +2 -2
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +45 -26
- package/dest/checkpoint_builder.d.ts +8 -1
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +52 -7
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +22 -1
- package/dest/duties/validation_service.d.ts +1 -1
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +2 -8
- package/dest/factory.js +1 -1
- package/dest/validator.d.ts +3 -3
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +37 -25
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +52 -33
- package/src/checkpoint_builder.ts +67 -8
- package/src/config.ts +22 -1
- package/src/duties/validation_service.ts +2 -8
- package/src/factory.ts +1 -1
- package/src/validator.ts +34 -28
package/dest/validator.js
CHANGED
|
@@ -9,11 +9,12 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
9
9
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
10
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
11
11
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
12
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
12
13
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
14
|
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
14
15
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
15
16
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
16
|
-
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
17
|
+
import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
|
|
17
18
|
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
18
19
|
import { EventEmitter } from 'events';
|
|
19
20
|
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
@@ -42,7 +43,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
42
43
|
l1ToL2MessageSource;
|
|
43
44
|
config;
|
|
44
45
|
blobClient;
|
|
45
|
-
|
|
46
|
+
slashingProtectionSigner;
|
|
46
47
|
dateProvider;
|
|
47
48
|
tracer;
|
|
48
49
|
validationService;
|
|
@@ -57,8 +58,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
57
58
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
|
|
58
59
|
proposersOfInvalidBlocks;
|
|
59
60
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
60
|
-
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient,
|
|
61
|
-
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.
|
|
61
|
+
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
62
|
+
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = new Set();
|
|
62
63
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
63
64
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
64
65
|
this.tracer = telemetry.getTracer('Validator');
|
|
@@ -117,26 +118,32 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
117
118
|
const metrics = new ValidatorMetrics(telemetry);
|
|
118
119
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
119
120
|
txsPermitted: !config.disableTransactions,
|
|
120
|
-
maxTxsPerBlock: config.
|
|
121
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock
|
|
121
122
|
});
|
|
122
123
|
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
|
|
123
124
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
124
|
-
let
|
|
125
|
-
let haSigner;
|
|
125
|
+
let slashingProtectionSigner;
|
|
126
126
|
if (config.haSigningEnabled) {
|
|
127
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
127
128
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
128
129
|
const haConfig = {
|
|
129
130
|
...config,
|
|
130
131
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
131
132
|
};
|
|
132
|
-
|
|
133
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
133
134
|
telemetryClient: telemetry,
|
|
134
135
|
dateProvider
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
}));
|
|
137
|
+
} else {
|
|
138
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
139
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
140
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
141
|
+
telemetryClient: telemetry,
|
|
142
|
+
dateProvider
|
|
143
|
+
}));
|
|
138
144
|
}
|
|
139
|
-
const
|
|
145
|
+
const validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
146
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider, telemetry);
|
|
140
147
|
return validator;
|
|
141
148
|
}
|
|
142
149
|
getValidatorAddresses() {
|
|
@@ -164,17 +171,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
164
171
|
};
|
|
165
172
|
}
|
|
166
173
|
reloadKeystore(newManager) {
|
|
167
|
-
if (this.config.haSigningEnabled && !this.haSigner) {
|
|
168
|
-
this.log.warn('HA signing is enabled in config but was not initialized at startup. ' + 'Restart the node to enable HA signing.');
|
|
169
|
-
} else if (!this.config.haSigningEnabled && this.haSigner) {
|
|
170
|
-
this.log.warn('HA signing was disabled via config update but the HA signer is still active. ' + 'Restart the node to fully disable HA signing.');
|
|
171
|
-
}
|
|
172
174
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
173
|
-
|
|
174
|
-
this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
|
|
175
|
-
} else {
|
|
176
|
-
this.keyStore = newAdapter;
|
|
177
|
-
}
|
|
175
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
178
176
|
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
179
177
|
}
|
|
180
178
|
async start() {
|
|
@@ -335,12 +333,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
335
333
|
const proposalInfo = {
|
|
336
334
|
slotNumber,
|
|
337
335
|
archive: proposal.archive.toString(),
|
|
338
|
-
proposer: proposer.toString()
|
|
339
|
-
txCount: proposal.txHashes.length
|
|
336
|
+
proposer: proposer.toString()
|
|
340
337
|
};
|
|
341
338
|
this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
|
|
342
339
|
...proposalInfo,
|
|
343
|
-
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
344
340
|
fishermanMode: this.config.fishermanMode || false
|
|
345
341
|
});
|
|
346
342
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
@@ -550,6 +546,22 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
550
546
|
reason: 'out_hash_mismatch'
|
|
551
547
|
};
|
|
552
548
|
}
|
|
549
|
+
// Final round of validations on the checkpoint, just in case.
|
|
550
|
+
try {
|
|
551
|
+
validateCheckpoint(computedCheckpoint, {
|
|
552
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
553
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
554
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
555
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
556
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
|
|
557
|
+
});
|
|
558
|
+
} catch (err) {
|
|
559
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
560
|
+
return {
|
|
561
|
+
isValid: false,
|
|
562
|
+
reason: 'checkpoint_validation_failed'
|
|
563
|
+
};
|
|
564
|
+
}
|
|
553
565
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
554
566
|
return {
|
|
555
567
|
isValid: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.04852196a",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,30 +64,30 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@aztec/blob-client": "0.0.1-commit.
|
|
68
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
69
|
-
"@aztec/constants": "0.0.1-commit.
|
|
70
|
-
"@aztec/epoch-cache": "0.0.1-commit.
|
|
71
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
72
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
73
|
-
"@aztec/node-keystore": "0.0.1-commit.
|
|
74
|
-
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.
|
|
75
|
-
"@aztec/p2p": "0.0.1-commit.
|
|
76
|
-
"@aztec/protocol-contracts": "0.0.1-commit.
|
|
77
|
-
"@aztec/prover-client": "0.0.1-commit.
|
|
78
|
-
"@aztec/simulator": "0.0.1-commit.
|
|
79
|
-
"@aztec/slasher": "0.0.1-commit.
|
|
80
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
81
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
82
|
-
"@aztec/validator-ha-signer": "0.0.1-commit.
|
|
67
|
+
"@aztec/blob-client": "0.0.1-commit.04852196a",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.04852196a",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.04852196a",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.04852196a",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.04852196a",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.04852196a",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.04852196a",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.04852196a",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.04852196a",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.04852196a",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.04852196a",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.04852196a",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.04852196a",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.04852196a",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.04852196a",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.04852196a",
|
|
83
83
|
"koa": "^2.16.1",
|
|
84
84
|
"koa-router": "^13.1.1",
|
|
85
85
|
"tslib": "^2.4.0",
|
|
86
86
|
"viem": "npm:@aztec/viem@2.38.2"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@aztec/archiver": "0.0.1-commit.
|
|
90
|
-
"@aztec/world-state": "0.0.1-commit.
|
|
89
|
+
"@aztec/archiver": "0.0.1-commit.04852196a",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.04852196a",
|
|
91
91
|
"@electric-sql/pglite": "^0.3.14",
|
|
92
92
|
"@jest/globals": "^30.0.0",
|
|
93
93
|
"@types/jest": "^30.0.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
3
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { pick } from '@aztec/foundation/collection';
|
|
4
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
6
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
6
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -10,6 +11,7 @@ import type { P2P, PeerId } from '@aztec/p2p';
|
|
|
10
11
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
11
12
|
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
12
13
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
14
|
+
import { Gas } from '@aztec/stdlib/gas';
|
|
13
15
|
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
14
16
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
15
17
|
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
@@ -87,25 +89,28 @@ export class BlockProposalHandler {
|
|
|
87
89
|
this.tracer = telemetry.getTracer('BlockProposalHandler');
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
// Non-validator handler that re-executes for monitoring but does not attest.
|
|
92
|
+
register(p2pClient: P2P, shouldReexecute: boolean): BlockProposalHandler {
|
|
93
|
+
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
92
94
|
// Returns boolean indicating whether the proposal was valid.
|
|
93
95
|
const handler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
|
|
94
96
|
try {
|
|
95
|
-
const
|
|
97
|
+
const { slotNumber, blockNumber } = proposal;
|
|
98
|
+
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
96
99
|
if (result.isValid) {
|
|
97
|
-
this.log.info(`Non-validator
|
|
100
|
+
this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
|
|
98
101
|
blockNumber: result.blockNumber,
|
|
102
|
+
slotNumber,
|
|
99
103
|
reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
|
|
100
104
|
totalManaUsed: result.reexecutionResult?.totalManaUsed,
|
|
101
105
|
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
|
|
106
|
+
reexecuted: shouldReexecute,
|
|
102
107
|
});
|
|
103
108
|
return true;
|
|
104
109
|
} else {
|
|
105
|
-
this.log.warn(
|
|
106
|
-
blockNumber
|
|
107
|
-
reason: result.reason,
|
|
108
|
-
|
|
110
|
+
this.log.warn(
|
|
111
|
+
`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`,
|
|
112
|
+
{ blockNumber: result.blockNumber, slotNumber, reason: result.reason },
|
|
113
|
+
);
|
|
109
114
|
return false;
|
|
110
115
|
}
|
|
111
116
|
} catch (error) {
|
|
@@ -184,6 +189,15 @@ export class BlockProposalHandler {
|
|
|
184
189
|
deadline: this.getReexecutionDeadline(slotNumber, config),
|
|
185
190
|
});
|
|
186
191
|
|
|
192
|
+
// If reexecution is disabled, bail. We are just interested in triggering tx collection.
|
|
193
|
+
if (!shouldReexecute) {
|
|
194
|
+
this.log.info(
|
|
195
|
+
`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
|
|
196
|
+
proposalInfo,
|
|
197
|
+
);
|
|
198
|
+
return { isValid: true, blockNumber };
|
|
199
|
+
}
|
|
200
|
+
|
|
187
201
|
// Compute the checkpoint number for this block and validate checkpoint consistency
|
|
188
202
|
const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
|
|
189
203
|
if (checkpointResult.reason) {
|
|
@@ -210,30 +224,28 @@ export class BlockProposalHandler {
|
|
|
210
224
|
return { isValid: false, blockNumber, reason: 'txs_not_available' };
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
228
|
+
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
229
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
230
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
231
|
+
.map(c => c.checkpointOutHash);
|
|
232
|
+
|
|
213
233
|
// Try re-executing the transactions in the proposal if needed
|
|
214
234
|
let reexecutionResult;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
l1ToL2Messages,
|
|
230
|
-
previousCheckpointOutHashes,
|
|
231
|
-
);
|
|
232
|
-
} catch (error) {
|
|
233
|
-
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
234
|
-
const reason = this.getReexecuteFailureReason(error);
|
|
235
|
-
return { isValid: false, blockNumber, reason, reexecutionResult };
|
|
236
|
-
}
|
|
235
|
+
try {
|
|
236
|
+
this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
|
|
237
|
+
reexecutionResult = await this.reexecuteTransactions(
|
|
238
|
+
proposal,
|
|
239
|
+
blockNumber,
|
|
240
|
+
checkpointNumber,
|
|
241
|
+
txs,
|
|
242
|
+
l1ToL2Messages,
|
|
243
|
+
previousCheckpointOutHashes,
|
|
244
|
+
);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
247
|
+
const reason = this.getReexecuteFailureReason(error);
|
|
248
|
+
return { isValid: false, blockNumber, reason, reexecutionResult };
|
|
237
249
|
}
|
|
238
250
|
|
|
239
251
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
@@ -242,8 +254,8 @@ export class BlockProposalHandler {
|
|
|
242
254
|
}
|
|
243
255
|
|
|
244
256
|
this.log.info(
|
|
245
|
-
`Successfully
|
|
246
|
-
proposalInfo,
|
|
257
|
+
`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
|
|
258
|
+
{ ...proposalInfo, ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed') },
|
|
247
259
|
);
|
|
248
260
|
|
|
249
261
|
return { isValid: true, blockNumber, reexecutionResult };
|
|
@@ -480,18 +492,25 @@ export class BlockProposalHandler {
|
|
|
480
492
|
|
|
481
493
|
// Build the new block
|
|
482
494
|
const deadline = this.getReexecutionDeadline(slot, config);
|
|
495
|
+
const maxBlockGas =
|
|
496
|
+
this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined
|
|
497
|
+
? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
|
|
498
|
+
: undefined;
|
|
483
499
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
484
500
|
deadline,
|
|
485
501
|
expectedEndState: blockHeader.state,
|
|
502
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
503
|
+
maxBlockGas,
|
|
486
504
|
});
|
|
487
505
|
|
|
488
506
|
const { block, failedTxs } = result;
|
|
489
507
|
const numFailedTxs = failedTxs.length;
|
|
490
508
|
|
|
491
|
-
this.log.verbose(`
|
|
509
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
492
510
|
numFailedTxs,
|
|
493
511
|
numProposalTxs: txHashes.length,
|
|
494
512
|
numProcessedTxs: block.body.txEffects.length,
|
|
513
|
+
blockNumber,
|
|
495
514
|
slot,
|
|
496
515
|
});
|
|
497
516
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
+
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT } from '@aztec/constants';
|
|
1
3
|
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
2
|
-
import { merge, pick } from '@aztec/foundation/collection';
|
|
4
|
+
import { merge, pick, sum } from '@aztec/foundation/collection';
|
|
3
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
6
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
5
7
|
import { bufferToHex } from '@aztec/foundation/string';
|
|
@@ -65,6 +67,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Builds a single block within this checkpoint.
|
|
70
|
+
* Automatically caps gas and blob field limits based on checkpoint-level budgets and prior blocks.
|
|
68
71
|
*/
|
|
69
72
|
async buildBlock(
|
|
70
73
|
pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
|
|
@@ -94,8 +97,14 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
94
97
|
});
|
|
95
98
|
const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork);
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
// Cap gas limits amd available blob fields by remaining checkpoint-level budgets
|
|
101
|
+
const cappedOpts: PublicProcessorLimits & { expectedEndState?: StateReference } = {
|
|
102
|
+
...opts,
|
|
103
|
+
...this.capLimitsByCheckpointBudgets(opts),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
|
|
107
|
+
processor.process(pendingTxs, cappedOpts, validator),
|
|
99
108
|
);
|
|
100
109
|
|
|
101
110
|
// Throw if we didn't collect a single valid tx and we're not allowed to build empty blocks
|
|
@@ -109,9 +118,6 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
109
118
|
expectedEndState: opts.expectedEndState,
|
|
110
119
|
});
|
|
111
120
|
|
|
112
|
-
// How much public gas was processed
|
|
113
|
-
const publicGas = processedTxs.reduce((acc, tx) => acc.add(tx.gasUsed.publicGas), Gas.empty());
|
|
114
|
-
|
|
115
121
|
this.log.debug('Built block within checkpoint', {
|
|
116
122
|
header: block.header.toInspect(),
|
|
117
123
|
processedTxs: processedTxs.map(tx => tx.hash.toString()),
|
|
@@ -120,12 +126,10 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
120
126
|
|
|
121
127
|
return {
|
|
122
128
|
block,
|
|
123
|
-
publicGas,
|
|
124
129
|
publicProcessorDuration,
|
|
125
130
|
numTxs: processedTxs.length,
|
|
126
131
|
failedTxs,
|
|
127
132
|
usedTxs,
|
|
128
|
-
usedTxBlobFields,
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -147,6 +151,61 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
147
151
|
return this.checkpointBuilder.clone().completeCheckpoint();
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Caps per-block gas and blob field limits by remaining checkpoint-level budgets.
|
|
156
|
+
* Computes remaining L2 gas (mana), DA gas, and blob fields from blocks already added to the checkpoint,
|
|
157
|
+
* then returns opts with maxBlockGas and maxBlobFields capped accordingly.
|
|
158
|
+
*/
|
|
159
|
+
protected capLimitsByCheckpointBudgets(
|
|
160
|
+
opts: PublicProcessorLimits,
|
|
161
|
+
): Pick<PublicProcessorLimits, 'maxBlockGas' | 'maxBlobFields' | 'maxTransactions'> {
|
|
162
|
+
const existingBlocks = this.checkpointBuilder.getBlocks();
|
|
163
|
+
|
|
164
|
+
// Remaining L2 gas (mana)
|
|
165
|
+
// IMPORTANT: This assumes mana is computed solely based on L2 gas used in transactions.
|
|
166
|
+
// This may change in the future.
|
|
167
|
+
const usedMana = sum(existingBlocks.map(b => b.header.totalManaUsed.toNumber()));
|
|
168
|
+
const remainingMana = this.config.rollupManaLimit - usedMana;
|
|
169
|
+
|
|
170
|
+
// Remaining DA gas
|
|
171
|
+
const usedDAGas = sum(existingBlocks.map(b => b.computeDAGasUsed())) ?? 0;
|
|
172
|
+
const remainingDAGas = MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT - usedDAGas;
|
|
173
|
+
|
|
174
|
+
// Remaining blob fields (block blob fields include both tx data and block-end overhead)
|
|
175
|
+
const usedBlobFields = sum(existingBlocks.map(b => b.toBlobFields().length));
|
|
176
|
+
const totalBlobCapacity = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
177
|
+
const isFirstBlock = existingBlocks.length === 0;
|
|
178
|
+
const blockEndOverhead = getNumBlockEndBlobFields(isFirstBlock);
|
|
179
|
+
const maxBlobFieldsForTxs = totalBlobCapacity - usedBlobFields - blockEndOverhead;
|
|
180
|
+
|
|
181
|
+
// Cap L2 gas by remaining checkpoint mana
|
|
182
|
+
const cappedL2Gas = Math.min(opts.maxBlockGas?.l2Gas ?? remainingMana, remainingMana);
|
|
183
|
+
|
|
184
|
+
// Cap DA gas by remaining checkpoint DA gas budget
|
|
185
|
+
const cappedDAGas = Math.min(opts.maxBlockGas?.daGas ?? remainingDAGas, remainingDAGas);
|
|
186
|
+
|
|
187
|
+
// Cap blob fields by remaining checkpoint blob capacity
|
|
188
|
+
const cappedBlobFields =
|
|
189
|
+
opts.maxBlobFields !== undefined ? Math.min(opts.maxBlobFields, maxBlobFieldsForTxs) : maxBlobFieldsForTxs;
|
|
190
|
+
|
|
191
|
+
// Cap transaction count by remaining checkpoint tx budget
|
|
192
|
+
let cappedMaxTransactions: number | undefined;
|
|
193
|
+
if (this.config.maxTxsPerCheckpoint !== undefined) {
|
|
194
|
+
const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
|
|
195
|
+
const remainingTxs = Math.max(0, this.config.maxTxsPerCheckpoint - usedTxs);
|
|
196
|
+
cappedMaxTransactions =
|
|
197
|
+
opts.maxTransactions !== undefined ? Math.min(opts.maxTransactions, remainingTxs) : remainingTxs;
|
|
198
|
+
} else {
|
|
199
|
+
cappedMaxTransactions = opts.maxTransactions;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
maxBlockGas: new Gas(cappedDAGas, cappedL2Gas),
|
|
204
|
+
maxBlobFields: cappedBlobFields,
|
|
205
|
+
maxTransactions: cappedMaxTransactions,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
150
209
|
protected async makeBlockBuilderDeps(globalVariables: GlobalVariables, fork: MerkleTreeWriteOperations) {
|
|
151
210
|
const txPublicSetupAllowList = [
|
|
152
211
|
...(await getDefaultAllowedSetupFunctions()),
|
package/src/config.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
secretValueConfigHelper,
|
|
7
7
|
} from '@aztec/foundation/config';
|
|
8
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
|
-
import { validatorHASignerConfigMappings } from '@aztec/stdlib/ha-signing';
|
|
9
|
+
import { localSignerConfigMappings, validatorHASignerConfigMappings } from '@aztec/stdlib/ha-signing';
|
|
10
10
|
import type { ValidatorClientConfig } from '@aztec/stdlib/interfaces/server';
|
|
11
11
|
|
|
12
12
|
export type { ValidatorClientConfig };
|
|
@@ -77,6 +77,27 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
|
|
|
77
77
|
description: 'Agree to attest to equivocated checkpoint proposals (for testing purposes only)',
|
|
78
78
|
...booleanConfigHelper(false),
|
|
79
79
|
},
|
|
80
|
+
validateMaxL2BlockGas: {
|
|
81
|
+
env: 'VALIDATOR_MAX_L2_BLOCK_GAS',
|
|
82
|
+
description: 'Maximum L2 block gas for validation. Proposals exceeding this limit are rejected.',
|
|
83
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
84
|
+
},
|
|
85
|
+
validateMaxDABlockGas: {
|
|
86
|
+
env: 'VALIDATOR_MAX_DA_BLOCK_GAS',
|
|
87
|
+
description: 'Maximum DA block gas for validation. Proposals exceeding this limit are rejected.',
|
|
88
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
89
|
+
},
|
|
90
|
+
validateMaxTxsPerBlock: {
|
|
91
|
+
env: 'VALIDATOR_MAX_TX_PER_BLOCK',
|
|
92
|
+
description: 'Maximum transactions per block for validation. Proposals exceeding this limit are rejected.',
|
|
93
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
94
|
+
},
|
|
95
|
+
validateMaxTxsPerCheckpoint: {
|
|
96
|
+
env: 'VALIDATOR_MAX_TX_PER_CHECKPOINT',
|
|
97
|
+
description: 'Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected.',
|
|
98
|
+
parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
|
|
99
|
+
},
|
|
100
|
+
...localSignerConfigMappings,
|
|
80
101
|
...validatorHASignerConfigMappings,
|
|
81
102
|
};
|
|
82
103
|
|
|
@@ -150,16 +150,10 @@ export class ValidationService {
|
|
|
150
150
|
);
|
|
151
151
|
|
|
152
152
|
// TODO(spy/ha): Use checkpointNumber instead of blockNumber once CheckpointHeader includes it.
|
|
153
|
-
//
|
|
153
|
+
// CheckpointProposalCore doesn't have lastBlock info, so use 0 as a proxy.
|
|
154
154
|
// blockNumber is NOT used for the primary key so it's safe to use here.
|
|
155
155
|
// See CheckpointHeader TODO and SigningContext types documentation.
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
blockNumber = proposal.blockNumber;
|
|
159
|
-
} catch {
|
|
160
|
-
// Checkpoint proposal may not have lastBlock, use 0 as fallback
|
|
161
|
-
blockNumber = BlockNumber(0);
|
|
162
|
-
}
|
|
156
|
+
const blockNumber = BlockNumber(0);
|
|
163
157
|
const context: SigningContext = {
|
|
164
158
|
slot: proposal.slotNumber,
|
|
165
159
|
blockNumber,
|
package/src/factory.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function createBlockProposalHandler(
|
|
|
29
29
|
const metrics = new ValidatorMetrics(deps.telemetry);
|
|
30
30
|
const blockProposalValidator = new BlockProposalValidator(deps.epochCache, {
|
|
31
31
|
txsPermitted: !config.disableTransactions,
|
|
32
|
-
maxTxsPerBlock: config.
|
|
32
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
33
33
|
});
|
|
34
34
|
return new BlockProposalHandler(
|
|
35
35
|
deps.checkpointsBuilder,
|