@aztec/validator-client 4.1.2 → 4.2.0-aztecnr-rc.2
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 +9 -10
- package/dest/block_proposal_handler.d.ts +64 -0
- package/dest/block_proposal_handler.d.ts.map +1 -0
- package/dest/{proposal_handler.js → block_proposal_handler.js} +9 -249
- package/dest/checkpoint_builder.d.ts +10 -8
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +29 -19
- package/dest/factory.d.ts +6 -5
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +4 -4
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/metrics.d.ts +2 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/validator.d.ts +22 -9
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +231 -18
- package/package.json +19 -19
- package/src/{proposal_handler.ts → block_proposal_handler.ts} +10 -283
- package/src/checkpoint_builder.ts +38 -25
- package/src/factory.ts +6 -5
- package/src/index.ts +1 -1
- package/src/metrics.ts +1 -1
- package/src/validator.ts +253 -21
- package/dest/proposal_handler.d.ts +0 -94
- package/dest/proposal_handler.d.ts.map +0 -1
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
|
-
import { type Blob, encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
4
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
5
|
-
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
6
3
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
7
4
|
import { pick } from '@aztec/foundation/collection';
|
|
8
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
9
6
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
10
|
-
import type { LogData } from '@aztec/foundation/log';
|
|
11
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
12
8
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
13
9
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
14
10
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
15
11
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
16
12
|
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
17
|
-
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
18
13
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
19
14
|
import { Gas } from '@aztec/stdlib/gas';
|
|
20
15
|
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
accumulateCheckpointOutHashes,
|
|
24
|
-
computeInHashFromL1ToL2Messages,
|
|
25
|
-
} from '@aztec/stdlib/messaging';
|
|
26
|
-
import type { BlockProposal, CheckpointProposalCore } from '@aztec/stdlib/p2p';
|
|
16
|
+
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
17
|
+
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
27
18
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
28
19
|
import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
29
20
|
import {
|
|
@@ -75,14 +66,11 @@ export type BlockProposalValidationFailureResult = {
|
|
|
75
66
|
|
|
76
67
|
export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
|
|
77
68
|
|
|
78
|
-
export type CheckpointProposalValidationResult = { isValid: true } | { isValid: false; reason: string };
|
|
79
|
-
|
|
80
69
|
type CheckpointComputationResult =
|
|
81
70
|
| { checkpointNumber: CheckpointNumber; reason?: undefined }
|
|
82
71
|
| { checkpointNumber?: undefined; reason: 'invalid_proposal' | 'global_variables_mismatch' };
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
export class ProposalHandler {
|
|
73
|
+
export class BlockProposalHandler {
|
|
86
74
|
public readonly tracer: Tracer;
|
|
87
75
|
|
|
88
76
|
constructor(
|
|
@@ -94,26 +82,21 @@ export class ProposalHandler {
|
|
|
94
82
|
private blockProposalValidator: BlockProposalValidator,
|
|
95
83
|
private epochCache: EpochCache,
|
|
96
84
|
private config: ValidatorClientFullConfig,
|
|
97
|
-
private blobClient: BlobClientInterface,
|
|
98
85
|
private metrics?: ValidatorMetrics,
|
|
99
86
|
private dateProvider: DateProvider = new DateProvider(),
|
|
100
87
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
101
|
-
private log = createLogger('validator:proposal-handler'),
|
|
88
|
+
private log = createLogger('validator:block-proposal-handler'),
|
|
102
89
|
) {
|
|
103
90
|
if (config.fishermanMode) {
|
|
104
91
|
this.log = this.log.createChild('[FISHERMAN]');
|
|
105
92
|
}
|
|
106
|
-
this.tracer = telemetry.getTracer('
|
|
93
|
+
this.tracer = telemetry.getTracer('BlockProposalHandler');
|
|
107
94
|
}
|
|
108
95
|
|
|
109
|
-
|
|
110
|
-
* Registers non-validator handlers for block and checkpoint proposals on the p2p client.
|
|
111
|
-
* Block proposals are always registered. Checkpoint proposals are registered if the blob client can upload.
|
|
112
|
-
*/
|
|
113
|
-
register(p2pClient: P2P, shouldReexecute: boolean): ProposalHandler {
|
|
96
|
+
register(p2pClient: P2P, shouldReexecute: boolean): BlockProposalHandler {
|
|
114
97
|
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
115
98
|
// Returns boolean indicating whether the proposal was valid.
|
|
116
|
-
const
|
|
99
|
+
const handler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
|
|
117
100
|
try {
|
|
118
101
|
const { slotNumber, blockNumber } = proposal;
|
|
119
102
|
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
@@ -140,35 +123,7 @@ export class ProposalHandler {
|
|
|
140
123
|
}
|
|
141
124
|
};
|
|
142
125
|
|
|
143
|
-
p2pClient.registerBlockProposalHandler(
|
|
144
|
-
|
|
145
|
-
// Register checkpoint proposal handler if blob uploads are enabled and we are reexecuting
|
|
146
|
-
if (this.blobClient.canUpload() && shouldReexecute) {
|
|
147
|
-
const checkpointHandler = async (checkpoint: CheckpointProposalCore, _sender: PeerId) => {
|
|
148
|
-
try {
|
|
149
|
-
const proposalInfo = {
|
|
150
|
-
proposalSlotNumber: checkpoint.slotNumber,
|
|
151
|
-
archive: checkpoint.archive.toString(),
|
|
152
|
-
proposer: checkpoint.getSender()?.toString(),
|
|
153
|
-
};
|
|
154
|
-
const result = await this.handleCheckpointProposal(checkpoint, proposalInfo);
|
|
155
|
-
if (result.isValid) {
|
|
156
|
-
this.log.info(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} handled`, proposalInfo);
|
|
157
|
-
} else {
|
|
158
|
-
this.log.warn(
|
|
159
|
-
`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} failed: ${result.reason}`,
|
|
160
|
-
proposalInfo,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
} catch (error) {
|
|
164
|
-
this.log.error('Error processing checkpoint proposal in non-validator handler', error);
|
|
165
|
-
}
|
|
166
|
-
// Non-validators don't attest
|
|
167
|
-
return undefined;
|
|
168
|
-
};
|
|
169
|
-
p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
170
|
-
}
|
|
171
|
-
|
|
126
|
+
p2pClient.registerBlockProposalHandler(handler);
|
|
172
127
|
return this;
|
|
173
128
|
}
|
|
174
129
|
|
|
@@ -614,6 +569,8 @@ export class ProposalHandler {
|
|
|
614
569
|
? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
|
|
615
570
|
: undefined;
|
|
616
571
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
572
|
+
isBuildingProposal: false,
|
|
573
|
+
minValidTxs: 0,
|
|
617
574
|
deadline,
|
|
618
575
|
expectedEndState: blockHeader.state,
|
|
619
576
|
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
@@ -668,234 +625,4 @@ export class ProposalHandler {
|
|
|
668
625
|
totalManaUsed,
|
|
669
626
|
};
|
|
670
627
|
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Validates a checkpoint proposal and uploads blobs if configured.
|
|
674
|
-
* Used by both non-validator nodes (via register) and the validator client (via delegation).
|
|
675
|
-
*/
|
|
676
|
-
async handleCheckpointProposal(
|
|
677
|
-
proposal: CheckpointProposalCore,
|
|
678
|
-
proposalInfo: LogData,
|
|
679
|
-
): Promise<CheckpointProposalValidationResult> {
|
|
680
|
-
const proposer = proposal.getSender();
|
|
681
|
-
if (!proposer) {
|
|
682
|
-
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
|
|
683
|
-
return { isValid: false, reason: 'invalid_signature' };
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
687
|
-
this.log.warn(
|
|
688
|
-
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`,
|
|
689
|
-
);
|
|
690
|
-
return { isValid: false, reason: 'invalid_fee_asset_price_modifier' };
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const result = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
694
|
-
|
|
695
|
-
// Upload blobs to filestore if validation passed (fire and forget)
|
|
696
|
-
if (result.isValid) {
|
|
697
|
-
this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
return result;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
|
|
705
|
-
* @returns Validation result with isValid flag and reason if invalid.
|
|
706
|
-
*/
|
|
707
|
-
async validateCheckpointProposal(
|
|
708
|
-
proposal: CheckpointProposalCore,
|
|
709
|
-
proposalInfo: LogData,
|
|
710
|
-
): Promise<CheckpointProposalValidationResult> {
|
|
711
|
-
const slot = proposal.slotNumber;
|
|
712
|
-
|
|
713
|
-
// Timeout block syncing at the start of the next slot
|
|
714
|
-
const config = this.checkpointsBuilder.getConfig();
|
|
715
|
-
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
716
|
-
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
717
|
-
|
|
718
|
-
// Wait for last block to sync by archive
|
|
719
|
-
let lastBlockHeader;
|
|
720
|
-
try {
|
|
721
|
-
lastBlockHeader = await retryUntil(
|
|
722
|
-
async () => {
|
|
723
|
-
await this.blockSource.syncImmediate();
|
|
724
|
-
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
725
|
-
},
|
|
726
|
-
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
727
|
-
timeoutSeconds,
|
|
728
|
-
0.5,
|
|
729
|
-
);
|
|
730
|
-
} catch (err) {
|
|
731
|
-
if (err instanceof TimeoutError) {
|
|
732
|
-
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
733
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
734
|
-
}
|
|
735
|
-
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
736
|
-
return { isValid: false, reason: 'block_fetch_error' };
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (!lastBlockHeader) {
|
|
740
|
-
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
741
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Get all full blocks for the slot and checkpoint
|
|
745
|
-
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
746
|
-
if (blocks.length === 0) {
|
|
747
|
-
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
748
|
-
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
752
|
-
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
753
|
-
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
754
|
-
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
758
|
-
...proposalInfo,
|
|
759
|
-
blockNumbers: blocks.map(b => b.number),
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
// Get checkpoint constants from first block
|
|
763
|
-
const firstBlock = blocks[0];
|
|
764
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
765
|
-
const checkpointNumber = firstBlock.checkpointNumber;
|
|
766
|
-
|
|
767
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
768
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
769
|
-
|
|
770
|
-
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
771
|
-
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
772
|
-
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
773
|
-
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
774
|
-
.map(c => c.checkpointOutHash);
|
|
775
|
-
|
|
776
|
-
// Fork world state at the block before the first block
|
|
777
|
-
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
778
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
779
|
-
|
|
780
|
-
try {
|
|
781
|
-
// Create checkpoint builder with all existing blocks
|
|
782
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
783
|
-
checkpointNumber,
|
|
784
|
-
constants,
|
|
785
|
-
proposal.feeAssetPriceModifier,
|
|
786
|
-
l1ToL2Messages,
|
|
787
|
-
previousCheckpointOutHashes,
|
|
788
|
-
fork,
|
|
789
|
-
blocks,
|
|
790
|
-
this.log.getBindings(),
|
|
791
|
-
);
|
|
792
|
-
|
|
793
|
-
// Complete the checkpoint to get computed values
|
|
794
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
795
|
-
|
|
796
|
-
// Compare checkpoint header with proposal
|
|
797
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
798
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
799
|
-
...proposalInfo,
|
|
800
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
801
|
-
proposal: proposal.checkpointHeader.toInspect(),
|
|
802
|
-
});
|
|
803
|
-
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Compare archive root with proposal
|
|
807
|
-
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
808
|
-
this.log.warn(`Archive root mismatch`, {
|
|
809
|
-
...proposalInfo,
|
|
810
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
811
|
-
proposal: proposal.archive.toString(),
|
|
812
|
-
});
|
|
813
|
-
return { isValid: false, reason: 'archive_mismatch' };
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
817
|
-
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
818
|
-
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
819
|
-
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
820
|
-
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
821
|
-
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
822
|
-
this.log.warn(`Epoch out hash mismatch`, {
|
|
823
|
-
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
824
|
-
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
825
|
-
checkpointOutHash: checkpointOutHash.toString(),
|
|
826
|
-
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
827
|
-
...proposalInfo,
|
|
828
|
-
});
|
|
829
|
-
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Final round of validations on the checkpoint, just in case.
|
|
833
|
-
try {
|
|
834
|
-
validateCheckpoint(computedCheckpoint, {
|
|
835
|
-
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
836
|
-
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
837
|
-
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
838
|
-
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
839
|
-
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
|
|
840
|
-
});
|
|
841
|
-
} catch (err) {
|
|
842
|
-
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
843
|
-
return { isValid: false, reason: 'checkpoint_validation_failed' };
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
847
|
-
return { isValid: true };
|
|
848
|
-
} finally {
|
|
849
|
-
await fork.close();
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/** Extracts checkpoint global variables from a block. */
|
|
854
|
-
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
855
|
-
const gv = block.header.globalVariables;
|
|
856
|
-
return {
|
|
857
|
-
chainId: gv.chainId,
|
|
858
|
-
version: gv.version,
|
|
859
|
-
slotNumber: gv.slotNumber,
|
|
860
|
-
timestamp: gv.timestamp,
|
|
861
|
-
coinbase: gv.coinbase,
|
|
862
|
-
feeRecipient: gv.feeRecipient,
|
|
863
|
-
gasFees: gv.gasFees,
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */
|
|
868
|
-
protected tryUploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): void {
|
|
869
|
-
if (this.blobClient.canUpload()) {
|
|
870
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/** Uploads blobs for a checkpoint to the filestore. */
|
|
875
|
-
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
876
|
-
try {
|
|
877
|
-
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
878
|
-
if (!lastBlockHeader) {
|
|
879
|
-
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
884
|
-
if (blocks.length === 0) {
|
|
885
|
-
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const blockBlobData = blocks.map(b => b.toBlockBlobData());
|
|
890
|
-
const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
|
|
891
|
-
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
892
|
-
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
893
|
-
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
894
|
-
...proposalInfo,
|
|
895
|
-
numBlobs: blobs.length,
|
|
896
|
-
});
|
|
897
|
-
} catch (err) {
|
|
898
|
-
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
628
|
}
|
|
@@ -20,6 +20,7 @@ import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
|
20
20
|
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
21
21
|
import { Gas } from '@aztec/stdlib/gas';
|
|
22
22
|
import {
|
|
23
|
+
type BlockBuilderOptions,
|
|
23
24
|
type BuildBlockInCheckpointResult,
|
|
24
25
|
type FullNodeBlockBuilderConfig,
|
|
25
26
|
FullNodeBlockBuilderConfigKeys,
|
|
@@ -46,6 +47,9 @@ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/serv
|
|
|
46
47
|
export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
47
48
|
private log: Logger;
|
|
48
49
|
|
|
50
|
+
/** Persistent contracts DB shared across all blocks in this checkpoint. */
|
|
51
|
+
protected contractsDB: PublicContractsDB;
|
|
52
|
+
|
|
49
53
|
constructor(
|
|
50
54
|
private checkpointBuilder: LightweightCheckpointBuilder,
|
|
51
55
|
private fork: MerkleTreeWriteOperations,
|
|
@@ -60,6 +64,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
60
64
|
...bindings,
|
|
61
65
|
instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
|
|
62
66
|
});
|
|
67
|
+
this.contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
getConstantData(): CheckpointGlobalVariables {
|
|
@@ -74,7 +79,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
74
79
|
pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
|
|
75
80
|
blockNumber: BlockNumber,
|
|
76
81
|
timestamp: bigint,
|
|
77
|
-
opts:
|
|
82
|
+
opts: BlockBuilderOptions & { expectedEndState?: StateReference },
|
|
78
83
|
): Promise<BuildBlockInCheckpointResult> {
|
|
79
84
|
const slot = this.checkpointBuilder.constants.slotNumber;
|
|
80
85
|
|
|
@@ -104,6 +109,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
104
109
|
...this.capLimitsByCheckpointBudgets(opts),
|
|
105
110
|
};
|
|
106
111
|
|
|
112
|
+
// Create a block-level checkpoint on the contracts DB so we can roll back on failure
|
|
113
|
+
this.contractsDB.createCheckpoint();
|
|
107
114
|
// We execute all merkle tree operations on a world state fork checkpoint
|
|
108
115
|
// This enables us to discard all modifications in the event that we fail to successfully process sufficient transactions
|
|
109
116
|
const forkCheckpoint = await ForkCheckpoint.new(this.fork);
|
|
@@ -112,6 +119,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
112
119
|
const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
|
|
113
120
|
processor.process(pendingTxs, cappedOpts, validator),
|
|
114
121
|
);
|
|
122
|
+
|
|
115
123
|
// Throw before updating state if we don't have enough valid txs
|
|
116
124
|
const minValidTxs = opts.minValidTxs ?? 0;
|
|
117
125
|
if (processedTxs.length < minValidTxs) {
|
|
@@ -126,6 +134,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
126
134
|
expectedEndState: opts.expectedEndState,
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
this.contractsDB.commitCheckpoint();
|
|
138
|
+
|
|
129
139
|
this.log.debug('Built block within checkpoint', {
|
|
130
140
|
header: block.header.toInspect(),
|
|
131
141
|
processedTxs: processedTxs.map(tx => tx.hash.toString()),
|
|
@@ -140,6 +150,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
140
150
|
usedTxs,
|
|
141
151
|
};
|
|
142
152
|
} catch (err) {
|
|
153
|
+
// Revert all changes to contracts db
|
|
154
|
+
this.contractsDB.revertCheckpoint();
|
|
143
155
|
// If we reached the point of committing the checkpoint, this does nothing
|
|
144
156
|
// Otherwise it reverts any changes made to the fork for this failed block
|
|
145
157
|
await forkCheckpoint.revert();
|
|
@@ -167,11 +179,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
167
179
|
|
|
168
180
|
/**
|
|
169
181
|
* Caps per-block gas and blob field limits by remaining checkpoint-level budgets.
|
|
170
|
-
*
|
|
171
|
-
*
|
|
182
|
+
* When building a proposal (isBuildingProposal=true), computes a fair share of remaining budget
|
|
183
|
+
* across remaining blocks scaled by the multiplier. When validating, only caps by per-block limit
|
|
184
|
+
* and remaining checkpoint budget (no redistribution or multiplier).
|
|
172
185
|
*/
|
|
173
186
|
protected capLimitsByCheckpointBudgets(
|
|
174
|
-
opts:
|
|
187
|
+
opts: BlockBuilderOptions,
|
|
175
188
|
): Pick<PublicProcessorLimits, 'maxBlockGas' | 'maxBlobFields' | 'maxTransactions'> {
|
|
176
189
|
const existingBlocks = this.checkpointBuilder.getBlocks();
|
|
177
190
|
|
|
@@ -192,31 +205,31 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
192
205
|
const blockEndOverhead = getNumBlockEndBlobFields(isFirstBlock);
|
|
193
206
|
const maxBlobFieldsForTxs = totalBlobCapacity - usedBlobFields - blockEndOverhead;
|
|
194
207
|
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
cappedMaxTransactions =
|
|
208
|
+
// Remaining txs
|
|
209
|
+
const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
|
|
210
|
+
const remainingTxs = Math.max(0, (this.config.maxTxsPerCheckpoint ?? Infinity) - usedTxs);
|
|
211
|
+
|
|
212
|
+
// Cap by per-block limit + remaining checkpoint budget
|
|
213
|
+
let cappedL2Gas = Math.min(opts.maxBlockGas?.l2Gas ?? Infinity, remainingMana);
|
|
214
|
+
let cappedDAGas = Math.min(opts.maxBlockGas?.daGas ?? Infinity, remainingDAGas);
|
|
215
|
+
let cappedBlobFields = Math.min(opts.maxBlobFields ?? Infinity, maxBlobFieldsForTxs);
|
|
216
|
+
let cappedMaxTransactions = Math.min(opts.maxTransactions ?? Infinity, remainingTxs);
|
|
217
|
+
|
|
218
|
+
// Proposer mode: further cap by fair share of remaining budget across remaining blocks
|
|
219
|
+
if (opts.isBuildingProposal) {
|
|
220
|
+
const remainingBlocks = Math.max(1, opts.maxBlocksPerCheckpoint - existingBlocks.length);
|
|
221
|
+
const multiplier = opts.perBlockAllocationMultiplier;
|
|
222
|
+
|
|
223
|
+
cappedL2Gas = Math.min(cappedL2Gas, Math.ceil((remainingMana / remainingBlocks) * multiplier));
|
|
224
|
+
cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * multiplier));
|
|
225
|
+
cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * multiplier));
|
|
226
|
+
cappedMaxTransactions = Math.min(cappedMaxTransactions, Math.ceil((remainingTxs / remainingBlocks) * multiplier));
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
return {
|
|
217
230
|
maxBlockGas: new Gas(cappedDAGas, cappedL2Gas),
|
|
218
231
|
maxBlobFields: cappedBlobFields,
|
|
219
|
-
maxTransactions: cappedMaxTransactions,
|
|
232
|
+
maxTransactions: Number.isFinite(cappedMaxTransactions) ? cappedMaxTransactions : undefined,
|
|
220
233
|
};
|
|
221
234
|
}
|
|
222
235
|
|
|
@@ -225,7 +238,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
225
238
|
...(await getDefaultAllowedSetupFunctions()),
|
|
226
239
|
...(this.config.txPublicSetupAllowListExtend ?? []),
|
|
227
240
|
];
|
|
228
|
-
const contractsDB =
|
|
241
|
+
const contractsDB = this.contractsDB;
|
|
229
242
|
const guardedFork = new GuardedMerkleTreeOperations(fork);
|
|
230
243
|
|
|
231
244
|
const collectDebugLogs = this.debugLogStore.isEnabled;
|
package/src/factory.ts
CHANGED
|
@@ -7,13 +7,14 @@ import type { L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
|
7
7
|
import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
8
8
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
9
9
|
import type { TelemetryClient } from '@aztec/telemetry-client';
|
|
10
|
+
import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
10
11
|
|
|
12
|
+
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
11
13
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
12
14
|
import { ValidatorMetrics } from './metrics.js';
|
|
13
|
-
import { ProposalHandler } from './proposal_handler.js';
|
|
14
15
|
import { ValidatorClient } from './validator.js';
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
+
export function createBlockProposalHandler(
|
|
17
18
|
config: ValidatorClientFullConfig,
|
|
18
19
|
deps: {
|
|
19
20
|
checkpointsBuilder: FullNodeCheckpointsBuilder;
|
|
@@ -22,7 +23,6 @@ export function createProposalHandler(
|
|
|
22
23
|
l1ToL2MessageSource: L1ToL2MessageSource;
|
|
23
24
|
p2pClient: P2PClient;
|
|
24
25
|
epochCache: EpochCache;
|
|
25
|
-
blobClient: BlobClientInterface;
|
|
26
26
|
dateProvider: DateProvider;
|
|
27
27
|
telemetry: TelemetryClient;
|
|
28
28
|
},
|
|
@@ -32,7 +32,7 @@ export function createProposalHandler(
|
|
|
32
32
|
txsPermitted: !config.disableTransactions,
|
|
33
33
|
maxTxsPerBlock: config.validateMaxTxsPerBlock ?? config.validateMaxTxsPerCheckpoint,
|
|
34
34
|
});
|
|
35
|
-
return new
|
|
35
|
+
return new BlockProposalHandler(
|
|
36
36
|
deps.checkpointsBuilder,
|
|
37
37
|
deps.worldState,
|
|
38
38
|
deps.blockSource,
|
|
@@ -41,7 +41,6 @@ export function createProposalHandler(
|
|
|
41
41
|
blockProposalValidator,
|
|
42
42
|
deps.epochCache,
|
|
43
43
|
config,
|
|
44
|
-
deps.blobClient,
|
|
45
44
|
metrics,
|
|
46
45
|
deps.dateProvider,
|
|
47
46
|
deps.telemetry,
|
|
@@ -61,6 +60,7 @@ export function createValidatorClient(
|
|
|
61
60
|
epochCache: EpochCache;
|
|
62
61
|
keyStoreManager: KeystoreManager | undefined;
|
|
63
62
|
blobClient: BlobClientInterface;
|
|
63
|
+
slashingProtectionDb?: SlashingProtectionDatabase;
|
|
64
64
|
},
|
|
65
65
|
) {
|
|
66
66
|
if (config.disableValidator || !deps.keyStoreManager) {
|
|
@@ -81,5 +81,6 @@ export function createValidatorClient(
|
|
|
81
81
|
deps.blobClient,
|
|
82
82
|
deps.dateProvider,
|
|
83
83
|
deps.telemetry,
|
|
84
|
+
deps.slashingProtectionDb,
|
|
84
85
|
);
|
|
85
86
|
}
|
package/src/index.ts
CHANGED
package/src/metrics.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createUpDownCounterWithDefault,
|
|
12
12
|
} from '@aztec/telemetry-client';
|
|
13
13
|
|
|
14
|
-
import type { BlockProposalValidationFailureReason } from './
|
|
14
|
+
import type { BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
15
15
|
|
|
16
16
|
export class ValidatorMetrics {
|
|
17
17
|
private failedReexecutionCounter: UpDownCounter;
|