@aztec/validator-client 0.0.1-fake-c83136db25 → 0.0.1-private.d0bedffd
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 +327 -0
- package/dest/block_proposal_handler.d.ts +26 -15
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +374 -109
- package/dest/checkpoint_builder.d.ts +76 -0
- package/dest/checkpoint_builder.d.ts.map +1 -0
- package/dest/checkpoint_builder.js +228 -0
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +41 -7
- package/dest/duties/validation_service.d.ts +42 -13
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +107 -31
- package/dest/factory.d.ts +13 -8
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +4 -3
- 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/ha_key_store.d.ts +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +2 -1
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +36 -6
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +10 -5
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +9 -5
- package/dest/key_store/node_keystore_adapter.d.ts +18 -5
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +18 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -11
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +13 -10
- package/dest/metrics.d.ts +12 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +52 -34
- package/dest/validator.d.ts +77 -22
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +537 -82
- package/package.json +24 -14
- package/src/block_proposal_handler.ts +298 -87
- package/src/checkpoint_builder.ts +390 -0
- package/src/config.ts +41 -6
- package/src/duties/validation_service.ts +158 -38
- package/src/factory.ts +18 -8
- package/src/index.ts +1 -0
- package/src/key_store/ha_key_store.ts +269 -0
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +44 -5
- package/src/key_store/local_key_store.ts +14 -5
- package/src/key_store/node_keystore_adapter.ts +28 -5
- package/src/key_store/web3signer_key_store.ts +22 -12
- package/src/metrics.ts +65 -33
- package/src/validator.ts +705 -115
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.0.1-
|
|
3
|
+
"version": "0.0.1-private.d0bedffd",
|
|
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,35 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@aztec/
|
|
68
|
-
"@aztec/
|
|
69
|
-
"@aztec/
|
|
70
|
-
"@aztec/
|
|
71
|
-
"@aztec/
|
|
72
|
-
"@aztec/
|
|
73
|
-
"@aztec/
|
|
74
|
-
"@aztec/
|
|
75
|
-
"@aztec/
|
|
76
|
-
"@aztec/
|
|
67
|
+
"@aztec/blob-client": "0.0.1-private.d0bedffd",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-private.d0bedffd",
|
|
69
|
+
"@aztec/constants": "0.0.1-private.d0bedffd",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-private.d0bedffd",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-private.d0bedffd",
|
|
72
|
+
"@aztec/foundation": "0.0.1-private.d0bedffd",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-private.d0bedffd",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-private.d0bedffd",
|
|
75
|
+
"@aztec/p2p": "0.0.1-private.d0bedffd",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-private.d0bedffd",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-private.d0bedffd",
|
|
78
|
+
"@aztec/simulator": "0.0.1-private.d0bedffd",
|
|
79
|
+
"@aztec/slasher": "0.0.1-private.d0bedffd",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-private.d0bedffd",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-private.d0bedffd",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-private.d0bedffd",
|
|
77
83
|
"koa": "^2.16.1",
|
|
78
84
|
"koa-router": "^13.1.1",
|
|
79
85
|
"tslib": "^2.4.0",
|
|
80
|
-
"viem": "npm:@
|
|
86
|
+
"viem": "npm:@aztec/viem@2.38.2"
|
|
81
87
|
},
|
|
82
88
|
"devDependencies": {
|
|
89
|
+
"@aztec/archiver": "0.0.1-private.d0bedffd",
|
|
90
|
+
"@aztec/world-state": "0.0.1-private.d0bedffd",
|
|
91
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
83
92
|
"@jest/globals": "^30.0.0",
|
|
84
93
|
"@types/jest": "^30.0.0",
|
|
85
94
|
"@types/node": "^22.15.17",
|
|
95
|
+
"@typescript/native-preview": "7.0.0-dev.20260113.1",
|
|
86
96
|
"jest": "^30.0.0",
|
|
87
97
|
"jest-mock-extended": "^4.0.0",
|
|
88
98
|
"ts-node": "^10.9.1",
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
+
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { pick } from '@aztec/foundation/collection';
|
|
5
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
2
6
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
3
|
-
import { Fr } from '@aztec/foundation/fields';
|
|
4
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
8
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
6
9
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
7
10
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
8
|
-
import { TxProvider } from '@aztec/p2p';
|
|
9
11
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import type {
|
|
14
|
-
import type
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
12
|
+
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
13
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
14
|
+
import { Gas } from '@aztec/stdlib/gas';
|
|
15
|
+
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
16
|
+
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
17
|
+
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
18
|
+
import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
17
19
|
import {
|
|
18
20
|
ReExFailedTxsError,
|
|
19
21
|
ReExStateMismatchError,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
} from '@aztec/stdlib/validators';
|
|
23
25
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
24
26
|
|
|
27
|
+
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
25
28
|
import type { ValidatorMetrics } from './metrics.js';
|
|
26
29
|
|
|
27
30
|
export type BlockProposalValidationFailureReason =
|
|
@@ -29,6 +32,7 @@ export type BlockProposalValidationFailureReason =
|
|
|
29
32
|
| 'parent_block_not_found'
|
|
30
33
|
| 'parent_block_wrong_slot'
|
|
31
34
|
| 'in_hash_mismatch'
|
|
35
|
+
| 'global_variables_mismatch'
|
|
32
36
|
| 'block_number_already_exists'
|
|
33
37
|
| 'txs_not_available'
|
|
34
38
|
| 'state_mismatch'
|
|
@@ -45,58 +49,74 @@ type ReexecuteTransactionsResult = {
|
|
|
45
49
|
|
|
46
50
|
export type BlockProposalValidationSuccessResult = {
|
|
47
51
|
isValid: true;
|
|
48
|
-
blockNumber:
|
|
52
|
+
blockNumber: BlockNumber;
|
|
49
53
|
reexecutionResult?: ReexecuteTransactionsResult;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
export type BlockProposalValidationFailureResult = {
|
|
53
57
|
isValid: false;
|
|
54
58
|
reason: BlockProposalValidationFailureReason;
|
|
55
|
-
blockNumber?:
|
|
59
|
+
blockNumber?: BlockNumber;
|
|
56
60
|
reexecutionResult?: ReexecuteTransactionsResult;
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
|
|
60
64
|
|
|
65
|
+
type CheckpointComputationResult =
|
|
66
|
+
| { checkpointNumber: CheckpointNumber; reason?: undefined }
|
|
67
|
+
| { checkpointNumber?: undefined; reason: 'invalid_proposal' | 'global_variables_mismatch' };
|
|
68
|
+
|
|
61
69
|
export class BlockProposalHandler {
|
|
62
70
|
public readonly tracer: Tracer;
|
|
63
71
|
|
|
64
72
|
constructor(
|
|
65
|
-
private
|
|
66
|
-
private
|
|
73
|
+
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
74
|
+
private worldState: WorldStateSynchronizer,
|
|
75
|
+
private blockSource: L2BlockSource & L2BlockSink,
|
|
67
76
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
68
|
-
private txProvider:
|
|
77
|
+
private txProvider: ITxProvider,
|
|
69
78
|
private blockProposalValidator: BlockProposalValidator,
|
|
79
|
+
private epochCache: EpochCache,
|
|
70
80
|
private config: ValidatorClientFullConfig,
|
|
71
81
|
private metrics?: ValidatorMetrics,
|
|
72
82
|
private dateProvider: DateProvider = new DateProvider(),
|
|
73
83
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
74
84
|
private log = createLogger('validator:block-proposal-handler'),
|
|
75
85
|
) {
|
|
86
|
+
if (config.fishermanMode) {
|
|
87
|
+
this.log = this.log.createChild('[FISHERMAN]');
|
|
88
|
+
}
|
|
76
89
|
this.tracer = telemetry.getTracer('BlockProposalHandler');
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
register(p2pClient: P2P, shouldReexecute: boolean): BlockProposalHandler {
|
|
93
|
+
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
94
|
+
// Returns boolean indicating whether the proposal was valid.
|
|
95
|
+
const handler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
|
|
81
96
|
try {
|
|
82
|
-
const
|
|
97
|
+
const { slotNumber, blockNumber } = proposal;
|
|
98
|
+
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
83
99
|
if (result.isValid) {
|
|
84
|
-
this.log.info(`Non-validator
|
|
100
|
+
this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
|
|
85
101
|
blockNumber: result.blockNumber,
|
|
102
|
+
slotNumber,
|
|
86
103
|
reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
|
|
87
104
|
totalManaUsed: result.reexecutionResult?.totalManaUsed,
|
|
88
105
|
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
|
|
106
|
+
reexecuted: shouldReexecute,
|
|
89
107
|
});
|
|
108
|
+
return true;
|
|
90
109
|
} else {
|
|
91
|
-
this.log.warn(
|
|
92
|
-
blockNumber
|
|
93
|
-
reason: result.reason,
|
|
94
|
-
|
|
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
|
+
);
|
|
114
|
+
return false;
|
|
95
115
|
}
|
|
96
116
|
} catch (error) {
|
|
97
117
|
this.log.error('Error processing block proposal in non-validator handler', error);
|
|
118
|
+
return false;
|
|
98
119
|
}
|
|
99
|
-
return undefined; // Non-validator nodes don't return attestations
|
|
100
120
|
};
|
|
101
121
|
|
|
102
122
|
p2pClient.registerBlockProposalHandler(handler);
|
|
@@ -108,9 +128,9 @@ export class BlockProposalHandler {
|
|
|
108
128
|
proposalSender: PeerId,
|
|
109
129
|
shouldReexecute: boolean,
|
|
110
130
|
): Promise<BlockProposalValidationResult> {
|
|
111
|
-
const slotNumber = proposal.slotNumber
|
|
131
|
+
const slotNumber = proposal.slotNumber;
|
|
112
132
|
const proposer = proposal.getSender();
|
|
113
|
-
const config = this.
|
|
133
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
114
134
|
|
|
115
135
|
// Reject proposals with invalid signatures
|
|
116
136
|
if (!proposer) {
|
|
@@ -126,23 +146,23 @@ export class BlockProposalHandler {
|
|
|
126
146
|
|
|
127
147
|
// Check that the proposal is from the current proposer, or the next proposer
|
|
128
148
|
// This should have been handled by the p2p layer, but we double check here out of caution
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
149
|
+
const validationResult = await this.blockProposalValidator.validate(proposal);
|
|
150
|
+
if (validationResult.result !== 'accept') {
|
|
131
151
|
this.log.warn(`Proposal is not valid, skipping processing`, proposalInfo);
|
|
132
152
|
return { isValid: false, reason: 'invalid_proposal' };
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
// Check that the parent proposal is a block we know, otherwise reexecution would fail
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
156
|
+
const parentBlock = await this.getParentBlock(proposal);
|
|
157
|
+
if (parentBlock === undefined) {
|
|
138
158
|
this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
|
|
139
159
|
return { isValid: false, reason: 'parent_block_not_found' };
|
|
140
160
|
}
|
|
141
161
|
|
|
142
|
-
// Check that the parent block's slot is
|
|
143
|
-
if (
|
|
144
|
-
this.log.warn(`Parent block slot is greater than
|
|
145
|
-
parentBlockSlot:
|
|
162
|
+
// Check that the parent block's slot is not greater than the proposal's slot.
|
|
163
|
+
if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
|
|
164
|
+
this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
|
|
165
|
+
parentBlockSlot: parentBlock.header.getSlot().toString(),
|
|
146
166
|
proposalSlot: slotNumber.toString(),
|
|
147
167
|
...proposalInfo,
|
|
148
168
|
});
|
|
@@ -150,7 +170,10 @@ export class BlockProposalHandler {
|
|
|
150
170
|
}
|
|
151
171
|
|
|
152
172
|
// Compute the block number based on the parent block
|
|
153
|
-
const blockNumber =
|
|
173
|
+
const blockNumber =
|
|
174
|
+
parentBlock === 'genesis'
|
|
175
|
+
? BlockNumber(INITIAL_L2_BLOCK_NUM)
|
|
176
|
+
: BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
154
177
|
|
|
155
178
|
// Check that this block number does not exist already
|
|
156
179
|
const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
|
|
@@ -166,10 +189,26 @@ export class BlockProposalHandler {
|
|
|
166
189
|
deadline: this.getReexecutionDeadline(slotNumber, config),
|
|
167
190
|
});
|
|
168
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
|
+
|
|
201
|
+
// Compute the checkpoint number for this block and validate checkpoint consistency
|
|
202
|
+
const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
|
|
203
|
+
if (checkpointResult.reason) {
|
|
204
|
+
return { isValid: false, blockNumber, reason: checkpointResult.reason };
|
|
205
|
+
}
|
|
206
|
+
const checkpointNumber = checkpointResult.checkpointNumber;
|
|
207
|
+
|
|
169
208
|
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
170
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(
|
|
171
|
-
const computedInHash =
|
|
172
|
-
const proposalInHash = proposal.
|
|
209
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
210
|
+
const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
211
|
+
const proposalInHash = proposal.inHash;
|
|
173
212
|
if (!computedInHash.equals(proposalInHash)) {
|
|
174
213
|
this.log.warn(`L1 to L2 messages in hash mismatch, skipping processing`, {
|
|
175
214
|
proposalInHash: proposalInHash.toString(),
|
|
@@ -185,27 +224,47 @@ export class BlockProposalHandler {
|
|
|
185
224
|
return { isValid: false, blockNumber, reason: 'txs_not_available' };
|
|
186
225
|
}
|
|
187
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
|
+
|
|
188
233
|
// Try re-executing the transactions in the proposal if needed
|
|
189
234
|
let reexecutionResult;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If we succeeded, push this block into the archiver (unless disabled)
|
|
252
|
+
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
253
|
+
await this.blockSource.addBlock(reexecutionResult?.block);
|
|
199
254
|
}
|
|
200
255
|
|
|
201
|
-
this.log.info(
|
|
256
|
+
this.log.info(
|
|
257
|
+
`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
|
|
258
|
+
{ ...proposalInfo, ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed') },
|
|
259
|
+
);
|
|
260
|
+
|
|
202
261
|
return { isValid: true, blockNumber, reexecutionResult };
|
|
203
262
|
}
|
|
204
263
|
|
|
205
|
-
private async getParentBlock(proposal: BlockProposal): Promise<'genesis' |
|
|
206
|
-
const parentArchive = proposal.
|
|
207
|
-
const slot = proposal.slotNumber
|
|
208
|
-
const config = this.
|
|
264
|
+
private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> {
|
|
265
|
+
const parentArchive = proposal.blockHeader.lastArchive.root;
|
|
266
|
+
const slot = proposal.slotNumber;
|
|
267
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
209
268
|
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
210
269
|
|
|
211
270
|
if (parentArchive.equals(genesisArchiveRoot)) {
|
|
@@ -218,12 +277,11 @@ export class BlockProposalHandler {
|
|
|
218
277
|
|
|
219
278
|
try {
|
|
220
279
|
return (
|
|
221
|
-
(await this.blockSource.
|
|
280
|
+
(await this.blockSource.getBlockDataByArchive(parentArchive)) ??
|
|
222
281
|
(timeoutDurationMs <= 0
|
|
223
282
|
? undefined
|
|
224
283
|
: await retryUntil(
|
|
225
|
-
() =>
|
|
226
|
-
this.blockSource.syncImmediate().then(() => this.blockSource.getBlockHeaderByArchive(parentArchive)),
|
|
284
|
+
() => this.blockSource.syncImmediate().then(() => this.blockSource.getBlockDataByArchive(parentArchive)),
|
|
227
285
|
'force archiver sync',
|
|
228
286
|
timeoutDurationMs / 1000,
|
|
229
287
|
0.5,
|
|
@@ -239,10 +297,132 @@ export class BlockProposalHandler {
|
|
|
239
297
|
}
|
|
240
298
|
}
|
|
241
299
|
|
|
242
|
-
private
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
private computeCheckpointNumber(
|
|
301
|
+
proposal: BlockProposal,
|
|
302
|
+
parentBlock: 'genesis' | BlockData,
|
|
303
|
+
proposalInfo: object,
|
|
304
|
+
): CheckpointComputationResult {
|
|
305
|
+
if (parentBlock === 'genesis') {
|
|
306
|
+
// First block is in checkpoint 1
|
|
307
|
+
if (proposal.indexWithinCheckpoint !== 0) {
|
|
308
|
+
this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
|
|
309
|
+
return { reason: 'invalid_proposal' };
|
|
310
|
+
}
|
|
311
|
+
return { checkpointNumber: CheckpointNumber.INITIAL };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (proposal.indexWithinCheckpoint === 0) {
|
|
315
|
+
// If this is the first block in a new checkpoint, increment the checkpoint number
|
|
316
|
+
if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
|
|
317
|
+
this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
|
|
318
|
+
return { reason: 'invalid_proposal' };
|
|
319
|
+
}
|
|
320
|
+
return { checkpointNumber: CheckpointNumber(parentBlock.checkpointNumber + 1) };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Otherwise it should follow the previous block in the same checkpoint
|
|
324
|
+
if (proposal.indexWithinCheckpoint !== parentBlock.indexWithinCheckpoint + 1) {
|
|
325
|
+
this.log.warn(`Non-sequential indexWithinCheckpoint`, proposalInfo);
|
|
326
|
+
return { reason: 'invalid_proposal' };
|
|
327
|
+
}
|
|
328
|
+
if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
|
|
329
|
+
this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
|
|
330
|
+
return { reason: 'invalid_proposal' };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// For non-first blocks in a checkpoint, validate global variables match parent (except blockNumber)
|
|
334
|
+
const validationResult = this.validateNonFirstBlockInCheckpoint(proposal, parentBlock, proposalInfo);
|
|
335
|
+
if (validationResult) {
|
|
336
|
+
return validationResult;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { checkpointNumber: parentBlock.checkpointNumber };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Validates that a non-first block in a checkpoint has consistent global variables with its parent.
|
|
344
|
+
* For blocks with indexWithinCheckpoint > 0, all global variables except blockNumber must match the parent.
|
|
345
|
+
* @returns A failure result if validation fails, undefined if validation passes
|
|
346
|
+
*/
|
|
347
|
+
private validateNonFirstBlockInCheckpoint(
|
|
348
|
+
proposal: BlockProposal,
|
|
349
|
+
parentBlock: BlockData,
|
|
350
|
+
proposalInfo: object,
|
|
351
|
+
): CheckpointComputationResult | undefined {
|
|
352
|
+
const proposalGlobals = proposal.blockHeader.globalVariables;
|
|
353
|
+
const parentGlobals = parentBlock.header.globalVariables;
|
|
354
|
+
|
|
355
|
+
// All global variables except blockNumber should match the parent
|
|
356
|
+
// blockNumber naturally increments between blocks
|
|
357
|
+
if (!proposalGlobals.chainId.equals(parentGlobals.chainId)) {
|
|
358
|
+
this.log.warn(`Non-first block in checkpoint has mismatched chainId`, {
|
|
359
|
+
...proposalInfo,
|
|
360
|
+
proposalChainId: proposalGlobals.chainId.toString(),
|
|
361
|
+
parentChainId: parentGlobals.chainId.toString(),
|
|
362
|
+
});
|
|
363
|
+
return { reason: 'global_variables_mismatch' };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!proposalGlobals.version.equals(parentGlobals.version)) {
|
|
367
|
+
this.log.warn(`Non-first block in checkpoint has mismatched version`, {
|
|
368
|
+
...proposalInfo,
|
|
369
|
+
proposalVersion: proposalGlobals.version.toString(),
|
|
370
|
+
parentVersion: parentGlobals.version.toString(),
|
|
371
|
+
});
|
|
372
|
+
return { reason: 'global_variables_mismatch' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (proposalGlobals.slotNumber !== parentGlobals.slotNumber) {
|
|
376
|
+
this.log.warn(`Non-first block in checkpoint has mismatched slotNumber`, {
|
|
377
|
+
...proposalInfo,
|
|
378
|
+
proposalSlotNumber: proposalGlobals.slotNumber,
|
|
379
|
+
parentSlotNumber: parentGlobals.slotNumber,
|
|
380
|
+
});
|
|
381
|
+
return { reason: 'global_variables_mismatch' };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (proposalGlobals.timestamp !== parentGlobals.timestamp) {
|
|
385
|
+
this.log.warn(`Non-first block in checkpoint has mismatched timestamp`, {
|
|
386
|
+
...proposalInfo,
|
|
387
|
+
proposalTimestamp: proposalGlobals.timestamp.toString(),
|
|
388
|
+
parentTimestamp: parentGlobals.timestamp.toString(),
|
|
389
|
+
});
|
|
390
|
+
return { reason: 'global_variables_mismatch' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!proposalGlobals.coinbase.equals(parentGlobals.coinbase)) {
|
|
394
|
+
this.log.warn(`Non-first block in checkpoint has mismatched coinbase`, {
|
|
395
|
+
...proposalInfo,
|
|
396
|
+
proposalCoinbase: proposalGlobals.coinbase.toString(),
|
|
397
|
+
parentCoinbase: parentGlobals.coinbase.toString(),
|
|
398
|
+
});
|
|
399
|
+
return { reason: 'global_variables_mismatch' };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!proposalGlobals.feeRecipient.equals(parentGlobals.feeRecipient)) {
|
|
403
|
+
this.log.warn(`Non-first block in checkpoint has mismatched feeRecipient`, {
|
|
404
|
+
...proposalInfo,
|
|
405
|
+
proposalFeeRecipient: proposalGlobals.feeRecipient.toString(),
|
|
406
|
+
parentFeeRecipient: parentGlobals.feeRecipient.toString(),
|
|
407
|
+
});
|
|
408
|
+
return { reason: 'global_variables_mismatch' };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!proposalGlobals.gasFees.equals(parentGlobals.gasFees)) {
|
|
412
|
+
this.log.warn(`Non-first block in checkpoint has mismatched gasFees`, {
|
|
413
|
+
...proposalInfo,
|
|
414
|
+
proposalGasFees: proposalGlobals.gasFees.toInspect(),
|
|
415
|
+
parentGasFees: parentGlobals.gasFees.toInspect(),
|
|
416
|
+
});
|
|
417
|
+
return { reason: 'global_variables_mismatch' };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private getReexecutionDeadline(slot: SlotNumber, config: { l1GenesisTime: bigint; slotDuration: number }): Date {
|
|
424
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
425
|
+
return new Date(nextSlotTimestampSeconds * 1000);
|
|
246
426
|
}
|
|
247
427
|
|
|
248
428
|
private getReexecuteFailureReason(err: any) {
|
|
@@ -259,12 +439,13 @@ export class BlockProposalHandler {
|
|
|
259
439
|
|
|
260
440
|
async reexecuteTransactions(
|
|
261
441
|
proposal: BlockProposal,
|
|
262
|
-
blockNumber:
|
|
442
|
+
blockNumber: BlockNumber,
|
|
443
|
+
checkpointNumber: CheckpointNumber,
|
|
263
444
|
txs: Tx[],
|
|
264
445
|
l1ToL2Messages: Fr[],
|
|
446
|
+
previousCheckpointOutHashes: Fr[],
|
|
265
447
|
): Promise<ReexecuteTransactionsResult> {
|
|
266
|
-
const {
|
|
267
|
-
const { txHashes } = proposal;
|
|
448
|
+
const { blockHeader, txHashes } = proposal;
|
|
268
449
|
|
|
269
450
|
// If we do not have all of the transactions, then we should fail
|
|
270
451
|
if (txs.length !== txHashes.length) {
|
|
@@ -273,32 +454,63 @@ export class BlockProposalHandler {
|
|
|
273
454
|
throw new TransactionsNotAvailableError(missingTxHashes);
|
|
274
455
|
}
|
|
275
456
|
|
|
276
|
-
// Use the sequencer's block building logic to re-execute the transactions
|
|
277
457
|
const timer = new Timer();
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
458
|
+
const slot = proposal.slotNumber;
|
|
459
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
460
|
+
|
|
461
|
+
// Get prior blocks in this checkpoint (same slot before current block)
|
|
462
|
+
const allBlocksInSlot = await this.blockSource.getBlocksForSlot(slot);
|
|
463
|
+
const priorBlocks = allBlocksInSlot.filter(b => b.number < blockNumber && b.header.getSlot() === slot);
|
|
464
|
+
|
|
465
|
+
// Fork before the block to be built
|
|
466
|
+
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
467
|
+
await this.worldState.syncImmediate(parentBlockNumber);
|
|
468
|
+
await using fork = await this.worldState.fork(parentBlockNumber);
|
|
469
|
+
|
|
470
|
+
// Build checkpoint constants from proposal (excludes blockNumber which is per-block)
|
|
471
|
+
const constants: CheckpointGlobalVariables = {
|
|
288
472
|
chainId: new Fr(config.l1ChainId),
|
|
289
473
|
version: new Fr(config.rollupVersion),
|
|
290
|
-
|
|
474
|
+
slotNumber: slot,
|
|
475
|
+
timestamp: blockHeader.globalVariables.timestamp,
|
|
476
|
+
coinbase: blockHeader.globalVariables.coinbase,
|
|
477
|
+
feeRecipient: blockHeader.globalVariables.feeRecipient,
|
|
478
|
+
gasFees: blockHeader.globalVariables.gasFees,
|
|
479
|
+
};
|
|
291
480
|
|
|
292
|
-
|
|
293
|
-
|
|
481
|
+
// Create checkpoint builder with prior blocks
|
|
482
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
483
|
+
checkpointNumber,
|
|
484
|
+
constants,
|
|
485
|
+
0n, // only takes effect in the following checkpoint.
|
|
486
|
+
l1ToL2Messages,
|
|
487
|
+
previousCheckpointOutHashes,
|
|
488
|
+
fork,
|
|
489
|
+
priorBlocks,
|
|
490
|
+
this.log.getBindings(),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
// Build the new block
|
|
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;
|
|
499
|
+
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
500
|
+
deadline,
|
|
501
|
+
expectedEndState: blockHeader.state,
|
|
502
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
503
|
+
maxBlockGas,
|
|
294
504
|
});
|
|
295
505
|
|
|
506
|
+
const { block, failedTxs } = result;
|
|
296
507
|
const numFailedTxs = failedTxs.length;
|
|
297
|
-
|
|
298
|
-
this.log.verbose(`
|
|
508
|
+
|
|
509
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
299
510
|
numFailedTxs,
|
|
300
511
|
numProposalTxs: txHashes.length,
|
|
301
512
|
numProcessedTxs: block.body.txEffects.length,
|
|
513
|
+
blockNumber,
|
|
302
514
|
slot,
|
|
303
515
|
});
|
|
304
516
|
|
|
@@ -313,19 +525,18 @@ export class BlockProposalHandler {
|
|
|
313
525
|
}
|
|
314
526
|
|
|
315
527
|
// Throw a ReExStateMismatchError error if state updates do not match
|
|
316
|
-
|
|
317
|
-
|
|
528
|
+
// Compare the full block structure (archive and header) from the built block with the proposal
|
|
529
|
+
const archiveMatches = proposal.archive.equals(block.archive.root);
|
|
530
|
+
const headerMatches = proposal.blockHeader.equals(block.header);
|
|
531
|
+
if (!archiveMatches || !headerMatches) {
|
|
318
532
|
this.log.warn(`Re-execution state mismatch for slot ${slot}`, {
|
|
319
|
-
|
|
320
|
-
|
|
533
|
+
expectedArchive: block.archive.root.toString(),
|
|
534
|
+
actualArchive: proposal.archive.toString(),
|
|
535
|
+
expectedHeader: block.header.toInspect(),
|
|
536
|
+
actualHeader: proposal.blockHeader.toInspect(),
|
|
321
537
|
});
|
|
322
538
|
this.metrics?.recordFailedReexecution(proposal);
|
|
323
|
-
throw new ReExStateMismatchError(
|
|
324
|
-
proposal.archive,
|
|
325
|
-
block.archive.root,
|
|
326
|
-
proposal.payload.stateReference,
|
|
327
|
-
block.header.state,
|
|
328
|
-
);
|
|
539
|
+
throw new ReExStateMismatchError(proposal.archive, block.archive.root);
|
|
329
540
|
}
|
|
330
541
|
|
|
331
542
|
const reexecutionTimeMs = timer.ms();
|