@aztec/archiver 0.0.1-commit.993d240 → 0.0.1-commit.9a89641
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/archiver.d.ts +44 -8
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +103 -35
- package/dest/config.d.ts +5 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +28 -7
- package/dest/factory.d.ts +8 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +41 -5
- package/dest/l1/calldata_retriever.d.ts +8 -1
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +7 -6
- package/dest/l1/data_retrieval.d.ts +7 -2
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/modules/contract_data_source_adapter.d.ts +2 -2
- package/dest/modules/contract_data_source_adapter.d.ts.map +1 -1
- package/dest/modules/contract_data_source_adapter.js +1 -9
- package/dest/modules/data_source_base.d.ts +5 -6
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +9 -38
- package/dest/modules/data_store_updater.d.ts +1 -1
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +4 -3
- package/dest/modules/instrumentation.d.ts +9 -1
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +23 -2
- package/dest/modules/l1_synchronizer.d.ts +3 -3
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +78 -41
- package/dest/modules/outbox_trees_resolver.d.ts +64 -0
- package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
- package/dest/modules/outbox_trees_resolver.js +184 -0
- package/dest/modules/validation.d.ts +29 -7
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +31 -13
- package/dest/store/block_store.d.ts +26 -22
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +105 -78
- package/dest/store/contract_class_store.d.ts +1 -1
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +19 -1
- package/dest/store/contract_instance_store.d.ts +1 -1
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +18 -1
- package/dest/store/log_store.d.ts +1 -1
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +3 -3
- package/dest/store/log_store_codec.d.ts +9 -1
- package/dest/store/log_store_codec.d.ts.map +1 -1
- package/dest/store/log_store_codec.js +9 -0
- package/dest/test/fake_l1_state.js +3 -3
- package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
- package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
- package/dest/test/mock_l1_to_l2_message_source.js +1 -2
- package/dest/test/mock_l2_block_source.d.ts +4 -11
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +15 -39
- package/dest/test/mock_structs.d.ts +1 -1
- package/dest/test/mock_structs.d.ts.map +1 -1
- package/dest/test/mock_structs.js +2 -2
- package/dest/test/noop_l1_archiver.d.ts +8 -3
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +12 -9
- package/package.json +13 -13
- package/src/archiver.ts +147 -42
- package/src/config.ts +34 -16
- package/src/factory.ts +48 -4
- package/src/l1/calldata_retriever.ts +13 -6
- package/src/l1/data_retrieval.ts +8 -1
- package/src/modules/contract_data_source_adapter.ts +1 -10
- package/src/modules/data_source_base.ts +17 -44
- package/src/modules/data_store_updater.ts +4 -3
- package/src/modules/instrumentation.ts +15 -2
- package/src/modules/l1_synchronizer.ts +120 -57
- package/src/modules/outbox_trees_resolver.ts +220 -0
- package/src/modules/validation.ts +71 -15
- package/src/store/block_store.ts +108 -101
- package/src/store/contract_class_store.ts +19 -1
- package/src/store/contract_instance_store.ts +18 -1
- package/src/store/log_store.ts +3 -2
- package/src/store/log_store_codec.ts +11 -0
- package/src/test/fake_l1_state.ts +3 -3
- package/src/test/mock_l1_to_l2_message_source.ts +0 -1
- package/src/test/mock_l2_block_source.ts +14 -53
- package/src/test/mock_structs.ts +5 -2
- package/src/test/noop_l1_archiver.ts +30 -11
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import {
|
|
2
|
+
import type { ViemCommitteeAttestations } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { type CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
3
4
|
import { compactArray } from '@aztec/foundation/collection';
|
|
5
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
6
|
import type { Logger } from '@aztec/foundation/log';
|
|
5
7
|
import {
|
|
6
8
|
type AttestationInfo,
|
|
9
|
+
type CommitteeAttestation,
|
|
7
10
|
type ValidateCheckpointNegativeResult,
|
|
8
11
|
type ValidateCheckpointResult,
|
|
9
12
|
getAttestationInfoFromPayload,
|
|
10
13
|
} from '@aztec/stdlib/block';
|
|
11
|
-
import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
14
|
+
import type { CheckpointInfo, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
12
15
|
import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
16
|
import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
17
|
+
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
14
18
|
|
|
15
19
|
export type { ValidateCheckpointResult };
|
|
16
20
|
|
|
@@ -26,28 +30,79 @@ export function getAttestationInfoFromPublishedCheckpoint(
|
|
|
26
30
|
return getAttestationInfoFromPayload(payload, attestations);
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
/** The subset of a calldata-only checkpoint needed to validate its committee attestations. */
|
|
34
|
+
export type CalldataCheckpointForAttestations = {
|
|
35
|
+
checkpointNumber: CheckpointNumber;
|
|
36
|
+
archiveRoot: Fr;
|
|
37
|
+
feeAssetPriceModifier: bigint;
|
|
38
|
+
header: CheckpointHeader;
|
|
39
|
+
attestations: CommitteeAttestation[];
|
|
40
|
+
/** The exact packed attestations tuple from L1 calldata, carried verbatim for byte-faithful invalidation. */
|
|
41
|
+
verbatimAttestations: ViemCommitteeAttestations;
|
|
42
|
+
};
|
|
43
|
+
|
|
29
44
|
/**
|
|
30
|
-
* Validates the attestations
|
|
31
|
-
*
|
|
45
|
+
* Validates the attestations of a checkpoint from L1 calldata only, without fetching or decoding its blobs.
|
|
46
|
+
* The signed consensus payload (header, archive root, fee asset price modifier) is fully available from
|
|
47
|
+
* calldata, so an invalid-attestation checkpoint can be rejected before any (possibly malformed) blob is
|
|
48
|
+
* fetched and decoded.
|
|
32
49
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
50
|
+
export function validateCheckpointAttestationsFromCalldata(
|
|
51
|
+
checkpoint: CalldataCheckpointForAttestations,
|
|
35
52
|
epochCache: EpochCache,
|
|
36
53
|
constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
37
54
|
signatureContext: CoordinationSignatureContext,
|
|
38
55
|
logger?: Logger,
|
|
39
56
|
): Promise<ValidateCheckpointResult> {
|
|
40
|
-
const
|
|
57
|
+
const payload = new ConsensusPayload(
|
|
58
|
+
checkpoint.header,
|
|
59
|
+
checkpoint.archiveRoot,
|
|
60
|
+
checkpoint.feeAssetPriceModifier,
|
|
61
|
+
signatureContext,
|
|
62
|
+
);
|
|
63
|
+
const checkpointInfo: CheckpointInfo = {
|
|
64
|
+
archive: checkpoint.archiveRoot,
|
|
65
|
+
lastArchive: checkpoint.header.lastArchiveRoot,
|
|
66
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
67
|
+
checkpointNumber: checkpoint.checkpointNumber,
|
|
68
|
+
timestamp: checkpoint.header.timestamp,
|
|
69
|
+
};
|
|
70
|
+
return validateAttestations(
|
|
71
|
+
payload,
|
|
72
|
+
checkpoint.attestations,
|
|
73
|
+
checkpoint.verbatimAttestations,
|
|
74
|
+
checkpointInfo,
|
|
75
|
+
epochCache,
|
|
76
|
+
constants,
|
|
77
|
+
logger,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Core attestation validation over a consensus payload, its attestations, and checkpoint metadata --
|
|
83
|
+
* independent of whether the checkpoint's blocks have been decoded from blobs. Returns true if the
|
|
84
|
+
* attestations are valid and sufficient, false otherwise.
|
|
85
|
+
*/
|
|
86
|
+
export async function validateAttestations(
|
|
87
|
+
payload: ConsensusPayload,
|
|
88
|
+
attestations: CommitteeAttestation[],
|
|
89
|
+
verbatimAttestations: ViemCommitteeAttestations,
|
|
90
|
+
checkpointInfo: CheckpointInfo,
|
|
91
|
+
epochCache: EpochCache,
|
|
92
|
+
constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
93
|
+
logger?: Logger,
|
|
94
|
+
): Promise<ValidateCheckpointResult> {
|
|
95
|
+
const attestorInfos = getAttestationInfoFromPayload(payload, attestations);
|
|
41
96
|
const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
97
|
+
const headerHash = payload.header.hash();
|
|
98
|
+
const archiveRoot = payload.archive.toString();
|
|
99
|
+
const slot = payload.header.slotNumber;
|
|
100
|
+
const checkpointNumber = checkpointInfo.checkpointNumber;
|
|
46
101
|
const epoch: EpochNumber = getEpochAtSlot(slot, constants);
|
|
47
102
|
const { committee, seed } = await epochCache.getCommitteeForEpoch(epoch);
|
|
48
|
-
const logData = { checkpointNumber
|
|
103
|
+
const logData = { checkpointNumber, slot, epoch, headerHash, archiveRoot };
|
|
49
104
|
|
|
50
|
-
logger?.debug(`Validating attestations for checkpoint ${
|
|
105
|
+
logger?.debug(`Validating attestations for checkpoint ${checkpointNumber} at slot ${slot} in epoch ${epoch}`, {
|
|
51
106
|
committee: (committee ?? []).map(member => member.toString()),
|
|
52
107
|
recoveredAttestors: attestorInfos,
|
|
53
108
|
postedAttestations: attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
|
|
@@ -72,12 +127,13 @@ export async function validateCheckpointAttestations(
|
|
|
72
127
|
const failedValidationResult = <TReason extends ValidateCheckpointNegativeResult['reason']>(reason: TReason) => ({
|
|
73
128
|
valid: false as const,
|
|
74
129
|
reason,
|
|
75
|
-
checkpoint:
|
|
130
|
+
checkpoint: checkpointInfo,
|
|
76
131
|
committee,
|
|
77
132
|
seed,
|
|
78
133
|
epoch,
|
|
79
134
|
attestors,
|
|
80
135
|
attestations,
|
|
136
|
+
verbatimAttestations,
|
|
81
137
|
});
|
|
82
138
|
|
|
83
139
|
for (let i = 0; i < attestorInfos.length; i++) {
|
|
@@ -123,7 +179,7 @@ export async function validateCheckpointAttestations(
|
|
|
123
179
|
}
|
|
124
180
|
|
|
125
181
|
logger?.debug(
|
|
126
|
-
`Checkpoint attestations validated successfully for checkpoint ${
|
|
182
|
+
`Checkpoint attestations validated successfully for checkpoint ${checkpointNumber} at slot ${slot}`,
|
|
127
183
|
logData,
|
|
128
184
|
);
|
|
129
185
|
return { valid: true };
|
package/src/store/block_store.ts
CHANGED
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
type ProposedCheckpointInput,
|
|
31
31
|
PublishedCheckpoint,
|
|
32
32
|
} from '@aztec/stdlib/checkpoint';
|
|
33
|
-
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
34
33
|
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
35
34
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
36
35
|
import {
|
|
@@ -38,8 +37,6 @@ import {
|
|
|
38
37
|
type IndexedTxEffect,
|
|
39
38
|
TxEffect,
|
|
40
39
|
TxHash,
|
|
41
|
-
TxReceipt,
|
|
42
|
-
TxStatus,
|
|
43
40
|
deserializeIndexedTxEffect,
|
|
44
41
|
serializeIndexedTxEffect,
|
|
45
42
|
} from '@aztec/stdlib/tx';
|
|
@@ -61,7 +58,7 @@ import {
|
|
|
61
58
|
ProposedCheckpointPromotionNotSequentialError,
|
|
62
59
|
} from '../errors.js';
|
|
63
60
|
|
|
64
|
-
export {
|
|
61
|
+
export type { TxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
|
|
65
62
|
|
|
66
63
|
type BlockIndexValue = [blockNumber: number, index: number];
|
|
67
64
|
|
|
@@ -309,12 +306,18 @@ export class BlockStore {
|
|
|
309
306
|
|
|
310
307
|
/**
|
|
311
308
|
* Append new checkpoints to the store's list.
|
|
309
|
+
* Checkpoints at the start of the batch that are already stored (e.g. re-included by an L1 reorg)
|
|
310
|
+
* are accepted if their archive root matches: their L1 metadata is updated but they are not
|
|
311
|
+
* re-inserted, and they are excluded from the returned array.
|
|
312
312
|
* @param checkpoints - The L2 checkpoints to be added to the store.
|
|
313
|
-
* @returns
|
|
313
|
+
* @returns The checkpoints that were actually inserted (excluding already-stored ones).
|
|
314
314
|
*/
|
|
315
|
-
async addCheckpoints(
|
|
315
|
+
async addCheckpoints(
|
|
316
|
+
checkpoints: PublishedCheckpoint[],
|
|
317
|
+
opts: { force?: boolean } = {},
|
|
318
|
+
): Promise<PublishedCheckpoint[]> {
|
|
316
319
|
if (checkpoints.length === 0) {
|
|
317
|
-
return
|
|
320
|
+
return [];
|
|
318
321
|
}
|
|
319
322
|
|
|
320
323
|
return await this.db.transactionAsync(async () => {
|
|
@@ -327,7 +330,7 @@ export class BlockStore {
|
|
|
327
330
|
if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
|
|
328
331
|
checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
|
|
329
332
|
if (checkpoints.length === 0) {
|
|
330
|
-
return
|
|
333
|
+
return [];
|
|
331
334
|
}
|
|
332
335
|
// Re-check sequentiality after skipping
|
|
333
336
|
const newFirstNumber = checkpoints[0].checkpoint.number;
|
|
@@ -390,7 +393,7 @@ export class BlockStore {
|
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
await this.advanceSynchedL1BlockNumber(checkpoints[checkpoints.length - 1].l1.blockNumber);
|
|
393
|
-
return
|
|
396
|
+
return checkpoints;
|
|
394
397
|
});
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -521,6 +524,7 @@ export class BlockStore {
|
|
|
521
524
|
l2BlockNumber: block.number,
|
|
522
525
|
l2BlockHash: blockHash,
|
|
523
526
|
txIndexInBlock: i,
|
|
527
|
+
slotNumber: block.header.globalVariables.slotNumber,
|
|
524
528
|
};
|
|
525
529
|
await this.#txEffects.set(txEffect.data.txHash.toString(), serializeIndexedTxEffect(txEffect));
|
|
526
530
|
}
|
|
@@ -533,20 +537,58 @@ export class BlockStore {
|
|
|
533
537
|
}
|
|
534
538
|
|
|
535
539
|
/** Deletes a block and all associated data (tx effects, indices). */
|
|
536
|
-
private async deleteBlock(
|
|
540
|
+
private async deleteBlock(blockNumber: number, blockStorage: BlockStorage): Promise<void> {
|
|
537
541
|
// Delete the block from the main blocks map
|
|
538
|
-
await this.#blocks.delete(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
+
await this.#blocks.delete(blockNumber);
|
|
543
|
+
|
|
544
|
+
const blockHash = bufferToHex(blockStorage.blockHash);
|
|
545
|
+
|
|
546
|
+
// Delete the tx effects of the block's txs, skipping entries that no longer point at this block: if
|
|
547
|
+
// another stored block also contains the tx, the entry points at that block, and deleting it here would
|
|
548
|
+
// orphan that block's index. Honest chains never store the same tx twice (duplicate nullifiers are
|
|
549
|
+
// rejected at build, re-execution, and world-state sync), so the store must not turn such a state into
|
|
550
|
+
// corruption of the surviving block if it ever appears.
|
|
551
|
+
const blockTxsBuffer = await this.#blockTxs.getAsync(blockHash);
|
|
552
|
+
if (blockTxsBuffer !== undefined) {
|
|
553
|
+
const reader = BufferReader.asReader(blockTxsBuffer);
|
|
554
|
+
const txHashes: string[] = [];
|
|
555
|
+
while (!reader.isEmpty()) {
|
|
556
|
+
txHashes.push(reader.readObject(TxHash).toString());
|
|
557
|
+
}
|
|
558
|
+
await Promise.all(txHashes.map(txHash => this.deleteTxEffect(txHash, blockStorage.blockHash)));
|
|
559
|
+
}
|
|
542
560
|
|
|
543
561
|
// Delete block txs mapping
|
|
544
|
-
const blockHash = (await block.hash()).toString();
|
|
545
562
|
await this.#blockTxs.delete(blockHash);
|
|
546
563
|
|
|
547
564
|
// Clean up indices
|
|
548
565
|
await this.#blockHashIndex.delete(blockHash);
|
|
549
|
-
await this.#blockArchiveIndex.delete(
|
|
566
|
+
await this.#blockArchiveIndex.delete(AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive).root.toString());
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Deletes a tx effect only if it is still owned by (points at) the given block. */
|
|
570
|
+
private async deleteTxEffect(txHash: string, blockHash: Buffer): Promise<void> {
|
|
571
|
+
const stored = await this.#txEffects.getAsync(txHash);
|
|
572
|
+
if (stored === undefined) {
|
|
573
|
+
this.#log.warn(`Missing tx effect for tx ${txHash} while removing its block`, {
|
|
574
|
+
txHash,
|
|
575
|
+
blockHash: bufferToHex(blockHash),
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// An IndexedTxEffect starts with the owning block hash (32 bytes) — see getTxLocation.
|
|
580
|
+
if (!Buffer.from(stored.buffer, stored.byteOffset, 32).equals(blockHash)) {
|
|
581
|
+
// Fires only when two stored blocks listed the same tx — a state upstream validation should make
|
|
582
|
+
// unreachable. Keep the entry (it belongs to the surviving block) but flag the duplication loudly.
|
|
583
|
+
this.#log.warn(`Tx effect for tx ${txHash} is owned by a block other than the one being removed`, {
|
|
584
|
+
txHash,
|
|
585
|
+
removedBlockHash: bufferToHex(blockHash),
|
|
586
|
+
owningBlockHash: bufferToHex(Buffer.from(stored.buffer, stored.byteOffset, 32)),
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
await this.#txEffects.delete(txHash);
|
|
550
592
|
}
|
|
551
593
|
|
|
552
594
|
/**
|
|
@@ -623,6 +665,26 @@ export class BlockStore {
|
|
|
623
665
|
return checkpoints;
|
|
624
666
|
}
|
|
625
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Returns up to `limit` checkpoints anchored at `fromSlot`, ordered nearest-first, walking the slot index.
|
|
670
|
+
* With `reverse`, takes the checkpoints at or before `fromSlot` (descending by slot); otherwise the
|
|
671
|
+
* checkpoints at or after it (ascending). `limit: 1, reverse: true` yields the latest checkpoint at or
|
|
672
|
+
* before the slot in a single range scan.
|
|
673
|
+
*/
|
|
674
|
+
async getCheckpointsBySlot(fromSlot: SlotNumber, limit: number, reverse: boolean): Promise<CheckpointData[]> {
|
|
675
|
+
// The KV range bounds are direction-dependent: forward is [start, end), reverse is (start, end], so a
|
|
676
|
+
// reverse scan uses `end: fromSlot` (inclusive) with no +1 to include the checkpoint at fromSlot itself.
|
|
677
|
+
const range = reverse ? { end: fromSlot, reverse: true, limit } : { start: fromSlot, limit };
|
|
678
|
+
const result: CheckpointData[] = [];
|
|
679
|
+
for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync(range)) {
|
|
680
|
+
const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
|
|
681
|
+
if (checkpointStorage) {
|
|
682
|
+
result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
|
|
626
688
|
/** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
|
|
627
689
|
async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
|
|
628
690
|
const result: CheckpointData[] = [];
|
|
@@ -725,16 +787,24 @@ export class BlockStore {
|
|
|
725
787
|
|
|
726
788
|
// Iterate from blockNumber + 1 to latestBlockNumber
|
|
727
789
|
for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
|
|
728
|
-
const
|
|
790
|
+
const blockStorage = await this.#blocks.getAsync(bn);
|
|
729
791
|
|
|
730
|
-
if (
|
|
792
|
+
if (blockStorage === undefined) {
|
|
731
793
|
this.#log.warn(`Cannot remove block ${bn} from the store since we don't have it`);
|
|
732
794
|
continue;
|
|
733
795
|
}
|
|
734
796
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
this
|
|
797
|
+
// Load the full block for the returned list when possible, but clean up from the raw row regardless:
|
|
798
|
+
// a block whose body can no longer be fully loaded must still release its row, tx effects, and indices,
|
|
799
|
+
// or later inserts at this number leave stale tx-effect entries pointing into the new chain.
|
|
800
|
+
const block = await this.getBlockFromBlockStorage(bn, blockStorage);
|
|
801
|
+
if (block !== undefined) {
|
|
802
|
+
removedBlocks.push(block);
|
|
803
|
+
} else {
|
|
804
|
+
this.#log.warn(`Removing block ${bn} whose body could not be fully loaded`);
|
|
805
|
+
}
|
|
806
|
+
await this.deleteBlock(bn, blockStorage);
|
|
807
|
+
this.#log.debug(`Removed block ${bn} ${bufferToHex(blockStorage.blockHash)}`);
|
|
738
808
|
}
|
|
739
809
|
|
|
740
810
|
return removedBlocks;
|
|
@@ -1092,56 +1162,6 @@ export class BlockStore {
|
|
|
1092
1162
|
return deserializeIndexedTxEffect(buffer);
|
|
1093
1163
|
}
|
|
1094
1164
|
|
|
1095
|
-
/**
|
|
1096
|
-
* Gets a receipt of a settled tx.
|
|
1097
|
-
* @param txHash - The hash of a tx we try to get the receipt for.
|
|
1098
|
-
* @returns The requested tx receipt (or undefined if not found).
|
|
1099
|
-
*/
|
|
1100
|
-
async getSettledTxReceipt(
|
|
1101
|
-
txHash: TxHash,
|
|
1102
|
-
l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
|
|
1103
|
-
): Promise<TxReceipt | undefined> {
|
|
1104
|
-
const txEffect = await this.getTxEffect(txHash);
|
|
1105
|
-
if (!txEffect) {
|
|
1106
|
-
return undefined;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const blockNumber = BlockNumber(txEffect.l2BlockNumber);
|
|
1110
|
-
|
|
1111
|
-
// Use existing archiver methods to determine finalization level
|
|
1112
|
-
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
|
|
1113
|
-
this.getProvenBlockNumber(),
|
|
1114
|
-
this.getCheckpointedL2BlockNumber(),
|
|
1115
|
-
this.getFinalizedL2BlockNumber(),
|
|
1116
|
-
this.getBlockData({ number: blockNumber }),
|
|
1117
|
-
]);
|
|
1118
|
-
|
|
1119
|
-
let status: TxStatus;
|
|
1120
|
-
if (blockNumber <= finalizedBlockNumber) {
|
|
1121
|
-
status = TxStatus.FINALIZED;
|
|
1122
|
-
} else if (blockNumber <= provenBlockNumber) {
|
|
1123
|
-
status = TxStatus.PROVEN;
|
|
1124
|
-
} else if (blockNumber <= checkpointedBlockNumber) {
|
|
1125
|
-
status = TxStatus.CHECKPOINTED;
|
|
1126
|
-
} else {
|
|
1127
|
-
status = TxStatus.PROPOSED;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
const epochNumber =
|
|
1131
|
-
blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
|
|
1132
|
-
|
|
1133
|
-
return new TxReceipt(
|
|
1134
|
-
txHash,
|
|
1135
|
-
status,
|
|
1136
|
-
TxReceipt.executionResultFromRevertCode(txEffect.data.revertCode),
|
|
1137
|
-
undefined,
|
|
1138
|
-
txEffect.data.transactionFee.toBigInt(),
|
|
1139
|
-
txEffect.l2BlockHash,
|
|
1140
|
-
blockNumber,
|
|
1141
|
-
epochNumber,
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
1165
|
/**
|
|
1146
1166
|
* Looks up which block included the requested tx effect.
|
|
1147
1167
|
* @param txHash - The txHash of the tx.
|
|
@@ -1167,8 +1187,9 @@ export class BlockStore {
|
|
|
1167
1187
|
* attach effect data on demand without paying for a full {@link TxEffect} deserialization.
|
|
1168
1188
|
*
|
|
1169
1189
|
* The on-disk `IndexedTxEffect` layout starts with a fixed-length header
|
|
1170
|
-
* (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4) +
|
|
1171
|
-
*
|
|
1190
|
+
* (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4) + slotNumber(4) + revertCode(1) + txHash(32) +
|
|
1191
|
+
* transactionFee(32)` = 109 bytes), followed by `noteHashes` and `nullifiers` (both u8-length-prefixed `Fr`
|
|
1192
|
+
* vectors). We
|
|
1172
1193
|
* skip the header, then read the two vectors, and stop — the large tail (`l2ToL1Msgs`,
|
|
1173
1194
|
* `publicDataWrites`, `privateLogs`, `publicLogs`, `contractClassLogs`) is never touched.
|
|
1174
1195
|
*/
|
|
@@ -1180,8 +1201,9 @@ export class BlockStore {
|
|
|
1180
1201
|
return [[], []];
|
|
1181
1202
|
}
|
|
1182
1203
|
const reader = BufferReader.asReader(buffer);
|
|
1183
|
-
// Skip the fixed-length header: blockHash + l2BlockNumber + txIndexInBlock +
|
|
1184
|
-
|
|
1204
|
+
// Skip the fixed-length header: blockHash + l2BlockNumber + txIndexInBlock + slotNumber + revertCode +
|
|
1205
|
+
// txHash + transactionFee.
|
|
1206
|
+
reader.readBytes(32 + 4 + 4 + 4 + 1 + 32 + 32);
|
|
1185
1207
|
const noteHashes = reader.readVectorUint8Prefix(Fr);
|
|
1186
1208
|
const nullifiers = reader.readVectorUint8Prefix(Fr);
|
|
1187
1209
|
return [noteHashes, nullifiers];
|
|
@@ -1217,14 +1239,13 @@ export class BlockStore {
|
|
|
1217
1239
|
}
|
|
1218
1240
|
|
|
1219
1241
|
/**
|
|
1220
|
-
* Resolves all
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* back to checkpointed when no pending checkpoint exists).
|
|
1242
|
+
* Resolves all four L2 chain tips (proposed, checkpointed, proven, finalized) in a single
|
|
1243
|
+
* read-only transaction so the snapshot is internally consistent. Each underlying record is
|
|
1244
|
+
* read at most once: latest block and latest confirmed checkpoint are loaded directly (no
|
|
1245
|
+
* separate "find the number, then look up data" hop), the proven/finalized checkpoint
|
|
1246
|
+
* singletons are read once and their storage entries are reused if they coincide with the
|
|
1247
|
+
* latest checkpoint, and per-tip block hashes are deduped when two tips land on the same block
|
|
1248
|
+
* (e.g. finalized == proven).
|
|
1228
1249
|
*
|
|
1229
1250
|
* The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by
|
|
1230
1251
|
* block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied
|
|
@@ -1247,9 +1268,6 @@ export class BlockStore {
|
|
|
1247
1268
|
|
|
1248
1269
|
// Load latest block and checkpoint entries
|
|
1249
1270
|
const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
|
|
1250
|
-
const [proposedCheckpointEntry] = await toArray(
|
|
1251
|
-
this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }),
|
|
1252
|
-
);
|
|
1253
1271
|
const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
|
|
1254
1272
|
const latestCheckpointNumber = latestCheckpointEntry
|
|
1255
1273
|
? CheckpointNumber(latestCheckpointEntry[0])
|
|
@@ -1335,14 +1353,6 @@ export class BlockStore {
|
|
|
1335
1353
|
const provenTip = await buildTipFromCheckpoint(provenCheckpoint);
|
|
1336
1354
|
const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint);
|
|
1337
1355
|
|
|
1338
|
-
// Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is
|
|
1339
|
-
// inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and
|
|
1340
|
-
// report the checkpointed tip as the proposed checkpoint to maintain the invariant.
|
|
1341
|
-
const proposedCheckpointTip =
|
|
1342
|
-
proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber
|
|
1343
|
-
? checkpointedTip
|
|
1344
|
-
: await buildTipFromCheckpoint(proposedCheckpointEntry[1]);
|
|
1345
|
-
|
|
1346
1356
|
// A checkpointed block past the latest stored block would mean a checkpoint
|
|
1347
1357
|
// references blocks that aren't in blocks.
|
|
1348
1358
|
if (proposedBlockId.number < checkpointedTip.block.number) {
|
|
@@ -1354,11 +1364,10 @@ export class BlockStore {
|
|
|
1354
1364
|
// Assert that checkpoint numbers are increasing
|
|
1355
1365
|
if (
|
|
1356
1366
|
finalizedTip.checkpoint.number > provenTip.checkpoint.number ||
|
|
1357
|
-
provenTip.checkpoint.number > checkpointedTip.checkpoint.number
|
|
1358
|
-
checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number
|
|
1367
|
+
provenTip.checkpoint.number > checkpointedTip.checkpoint.number
|
|
1359
1368
|
) {
|
|
1360
1369
|
throw new Error(
|
|
1361
|
-
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}
|
|
1370
|
+
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}`,
|
|
1362
1371
|
);
|
|
1363
1372
|
}
|
|
1364
1373
|
|
|
@@ -1366,17 +1375,15 @@ export class BlockStore {
|
|
|
1366
1375
|
if (
|
|
1367
1376
|
finalizedTip.block.number > provenTip.block.number ||
|
|
1368
1377
|
provenTip.block.number > checkpointedTip.block.number ||
|
|
1369
|
-
checkpointedTip.block.number >
|
|
1370
|
-
proposedCheckpointTip.block.number > proposedBlockId.number
|
|
1378
|
+
checkpointedTip.block.number > proposedBlockId.number
|
|
1371
1379
|
) {
|
|
1372
1380
|
throw new Error(
|
|
1373
|
-
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number}
|
|
1381
|
+
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposed=${proposedBlockId.number}`,
|
|
1374
1382
|
);
|
|
1375
1383
|
}
|
|
1376
1384
|
|
|
1377
1385
|
return {
|
|
1378
1386
|
proposed: proposedBlockId,
|
|
1379
|
-
proposedCheckpoint: proposedCheckpointTip,
|
|
1380
1387
|
checkpointed: checkpointedTip,
|
|
1381
1388
|
proven: provenTip,
|
|
1382
1389
|
finalized: finalizedTip,
|
|
@@ -2,6 +2,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
|
|
|
2
2
|
import { toArray } from '@aztec/foundation/iterable';
|
|
3
3
|
import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize';
|
|
4
4
|
import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
|
|
5
|
+
import { isProtocolContractClass } from '@aztec/protocol-contracts';
|
|
5
6
|
import type {
|
|
6
7
|
ContractClassPublic,
|
|
7
8
|
ContractClassPublicWithBlockNumber,
|
|
@@ -49,7 +50,19 @@ export class ContractClassStore {
|
|
|
49
50
|
): Promise<void> {
|
|
50
51
|
await this.db.transactionAsync(async () => {
|
|
51
52
|
const key = contractClass.id.toString();
|
|
52
|
-
|
|
53
|
+
const existing = await this.#contractClasses.getAsync(key);
|
|
54
|
+
if (existing !== undefined) {
|
|
55
|
+
// Protocol contracts are preloaded at block 0, so a later on-chain (re-)publish of a bundled
|
|
56
|
+
// protocol class id is valid and must be a no-op. Keep the existing block-0 entry untouched
|
|
57
|
+
// (do not bump its block number) so it survives reorgs of the publishing block.
|
|
58
|
+
if (isProtocolContractClass(contractClass.id)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// An L1 reorg can re-present an already-stored checkpoint, replaying this class at the same
|
|
62
|
+
// block; treat that as a no-op. A duplicate at a different block still signals double-processing.
|
|
63
|
+
if (deserializeContractClassPublic(existing).l2BlockNumber === blockNumber) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
53
66
|
throw new Error(`Contract class ${key} already exists, cannot add again at block ${blockNumber}`);
|
|
54
67
|
}
|
|
55
68
|
await this.#contractClasses.set(
|
|
@@ -61,6 +74,11 @@ export class ContractClassStore {
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
async deleteContractClass(contractClass: ContractClassPublic, blockNumber: number): Promise<void> {
|
|
77
|
+
// Protocol contracts are preloaded at block 0 and must never be deleted, even when the block that
|
|
78
|
+
// (re-)published them on-chain is unwound by a reorg.
|
|
79
|
+
if (isProtocolContractClass(contractClass.id)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
64
82
|
const restoredContractClass = await this.#contractClasses.getAsync(contractClass.id.toString());
|
|
65
83
|
if (restoredContractClass && deserializeContractClassPublic(restoredContractClass).l2BlockNumber >= blockNumber) {
|
|
66
84
|
await this.db.transactionAsync(async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
2
2
|
import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
|
|
3
|
+
import { isProtocolContract } from '@aztec/protocol-contracts';
|
|
3
4
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
4
5
|
import {
|
|
5
6
|
type ContractInstanceUpdateWithAddress,
|
|
@@ -72,8 +73,19 @@ export class ContractInstanceStore {
|
|
|
72
73
|
return this.db.transactionAsync(async () => {
|
|
73
74
|
const key = contractInstance.address.toString();
|
|
74
75
|
if (await this.#contractInstances.hasAsync(key)) {
|
|
76
|
+
// Protocol contracts are preloaded at block 0, so a later on-chain (re-)publish of a bundled
|
|
77
|
+
// protocol instance is valid and must be a no-op. Keep the existing block-0 entry untouched.
|
|
78
|
+
if (isProtocolContract(contractInstance.address)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const existingBlockNumber = await this.#contractInstancePublishedAt.getAsync(key);
|
|
82
|
+
// An L1 reorg can re-present an already-stored checkpoint, replaying this instance at the same
|
|
83
|
+
// block; treat that as a no-op. A duplicate at a different block still signals double-processing.
|
|
84
|
+
if (existingBlockNumber === blockNumber) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
75
87
|
throw new Error(
|
|
76
|
-
`Contract instance at ${key} already exists (deployed at block ${
|
|
88
|
+
`Contract instance at ${key} already exists (deployed at block ${existingBlockNumber}), cannot add again at block ${blockNumber}`,
|
|
77
89
|
);
|
|
78
90
|
}
|
|
79
91
|
await this.#contractInstances.set(key, new SerializableContractInstance(contractInstance).toBuffer());
|
|
@@ -82,6 +94,11 @@ export class ContractInstanceStore {
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
deleteContractInstance(contractInstance: ContractInstanceWithAddress): Promise<void> {
|
|
97
|
+
// Protocol contracts are preloaded at block 0 and must never be deleted, even when the block that
|
|
98
|
+
// (re-)published them on-chain is unwound by a reorg.
|
|
99
|
+
if (isProtocolContract(contractInstance.address)) {
|
|
100
|
+
return Promise.resolve();
|
|
101
|
+
}
|
|
85
102
|
return this.db.transactionAsync(async () => {
|
|
86
103
|
await this.#contractInstances.delete(contractInstance.address.toString());
|
|
87
104
|
await this.#contractInstancePublishedAt.delete(contractInstance.address.toString());
|
package/src/store/log_store.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
endOfTxRange,
|
|
27
27
|
fieldHex,
|
|
28
28
|
incKey,
|
|
29
|
+
tagHexForLog,
|
|
29
30
|
} from './log_store_codec.js';
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -100,7 +101,7 @@ export class LogStore {
|
|
|
100
101
|
let publicLogIndexWithinTx = 0;
|
|
101
102
|
|
|
102
103
|
for (const log of txEffect.privateLogs) {
|
|
103
|
-
const tagHex =
|
|
104
|
+
const tagHex = tagHexForLog(log.fields);
|
|
104
105
|
const key = encodeKey(tagHex, blockNumber, txIndexWithinBlock, privateLogIndexWithinTx);
|
|
105
106
|
const value = encodeValue({
|
|
106
107
|
txHash,
|
|
@@ -115,7 +116,7 @@ export class LogStore {
|
|
|
115
116
|
|
|
116
117
|
for (const log of txEffect.publicLogs) {
|
|
117
118
|
const contractHex = fieldHex(log.contractAddress);
|
|
118
|
-
const tagHex =
|
|
119
|
+
const tagHex = tagHexForLog(log.fields);
|
|
119
120
|
const key = encodeKey(
|
|
120
121
|
encodePublicPrefix(contractHex, tagHex),
|
|
121
122
|
blockNumber,
|
|
@@ -37,6 +37,17 @@ export function fieldHex(value: Fr | { toString: () => string }): string {
|
|
|
37
37
|
return value.toString().slice(2);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Tag prefix for a log: the hex of its first field, or the empty string when the log carries no fields.
|
|
42
|
+
* A protocol-valid public log can have zero fields (e.g. a raw AVM `EMITPUBLICLOG` with `logSize = 0`),
|
|
43
|
+
* which has no tag to index by. Encoding it under the empty tag keeps it retrievable via the per-block
|
|
44
|
+
* read (and droppable on reorg) while never matching a real 64-hex-char tag query — instead of reading
|
|
45
|
+
* `fields[0]` off an empty array and aborting the whole block-ingestion transaction.
|
|
46
|
+
*/
|
|
47
|
+
export function tagHexForLog(fields: Fr[]): string {
|
|
48
|
+
return fields.length > 0 ? fieldHex(fields[0]) : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
/** Encodes a number as 8-char zero-padded lowercase hex (matches a u32 big-endian byte buffer's lex order). */
|
|
41
52
|
export function u32Hex(n: number): string {
|
|
42
53
|
return n.toString(16).padStart(NUMERIC_HEX_LEN, '0');
|
|
@@ -682,7 +682,7 @@ export class FakeL1State {
|
|
|
682
682
|
getHashedSignaturePayloadTypedData(attestationsAndSigners),
|
|
683
683
|
);
|
|
684
684
|
|
|
685
|
-
const
|
|
685
|
+
const verbatimAttestations = attestationsAndSigners.getPackedAttestations();
|
|
686
686
|
|
|
687
687
|
const rollupInput = encodeFunctionData({
|
|
688
688
|
abi: RollupAbi,
|
|
@@ -693,7 +693,7 @@ export class FakeL1State {
|
|
|
693
693
|
archive,
|
|
694
694
|
oracleInput: { feeAssetPriceModifier: 0n },
|
|
695
695
|
},
|
|
696
|
-
|
|
696
|
+
verbatimAttestations,
|
|
697
697
|
attestationsAndSigners.getSigners().map(signer => signer.toString()),
|
|
698
698
|
attestationsAndSignersSignature.toViemSignature(),
|
|
699
699
|
blobInput,
|
|
@@ -716,7 +716,7 @@ export class FakeL1State {
|
|
|
716
716
|
|
|
717
717
|
// Compute attestationsHash (same logic as CalldataRetriever)
|
|
718
718
|
const attestationsHash = Buffer32.fromString(
|
|
719
|
-
keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [
|
|
719
|
+
keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations])),
|
|
720
720
|
);
|
|
721
721
|
|
|
722
722
|
// Compute payloadDigest (same logic as CalldataRetriever)
|