@aztec/archiver 0.0.1-commit.d3ec352c → 0.0.1-commit.d58ff9d0
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 +164 -22
- package/dest/archiver.d.ts +194 -0
- package/dest/archiver.d.ts.map +1 -0
- package/dest/archiver.js +949 -0
- package/dest/config.d.ts +34 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +95 -0
- package/dest/errors.d.ts +87 -0
- package/dest/errors.d.ts.map +1 -0
- package/dest/errors.js +129 -0
- package/dest/factory.d.ts +16 -10
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +117 -20
- package/dest/index.d.ts +19 -4
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +17 -3
- package/dest/interfaces.d.ts +9 -0
- package/dest/interfaces.d.ts.map +1 -0
- package/dest/interfaces.js +3 -0
- package/dest/l1/bin/retrieve-calldata.d.ts +3 -0
- package/dest/l1/bin/retrieve-calldata.d.ts.map +1 -0
- package/dest/l1/bin/retrieve-calldata.js +152 -0
- package/dest/l1/calldata_retriever.d.ts +143 -0
- package/dest/l1/calldata_retriever.d.ts.map +1 -0
- package/dest/l1/calldata_retriever.js +413 -0
- package/dest/l1/data_retrieval.d.ts +102 -0
- package/dest/l1/data_retrieval.d.ts.map +1 -0
- package/dest/{archiver → l1}/data_retrieval.js +85 -165
- package/dest/l1/debug_tx.d.ts +19 -0
- package/dest/l1/debug_tx.d.ts.map +1 -0
- package/dest/l1/debug_tx.js +73 -0
- package/dest/l1/spire_proposer.d.ts +70 -0
- package/dest/l1/spire_proposer.d.ts.map +1 -0
- package/dest/l1/spire_proposer.js +149 -0
- package/dest/l1/trace_tx.d.ts +43 -0
- package/dest/l1/trace_tx.d.ts.map +1 -0
- package/dest/l1/trace_tx.js +91 -0
- package/dest/l1/types.d.ts +12 -0
- package/dest/l1/types.d.ts.map +1 -0
- package/dest/l1/types.js +3 -0
- package/dest/l1/validate_historical_logs.d.ts +23 -0
- package/dest/l1/validate_historical_logs.d.ts.map +1 -0
- package/dest/l1/validate_historical_logs.js +108 -0
- package/dest/l1/validate_trace.d.ts +32 -0
- package/dest/l1/validate_trace.d.ts.map +1 -0
- package/dest/l1/validate_trace.js +154 -0
- package/dest/modules/contract_data_source_adapter.d.ts +25 -0
- package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
- package/dest/modules/contract_data_source_adapter.js +32 -0
- package/dest/modules/data_source_base.d.ts +112 -0
- package/dest/modules/data_source_base.d.ts.map +1 -0
- package/dest/modules/data_source_base.js +322 -0
- package/dest/modules/data_store_updater.d.ts +105 -0
- package/dest/modules/data_store_updater.d.ts.map +1 -0
- package/dest/modules/data_store_updater.js +393 -0
- package/dest/modules/instrumentation.d.ts +63 -0
- package/dest/modules/instrumentation.d.ts.map +1 -0
- package/dest/modules/instrumentation.js +166 -0
- package/dest/modules/l1_synchronizer.d.ts +77 -0
- package/dest/modules/l1_synchronizer.d.ts.map +1 -0
- package/dest/modules/l1_synchronizer.js +1381 -0
- package/dest/modules/outbox_trees_resolver.d.ts +62 -0
- package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
- package/dest/modules/outbox_trees_resolver.js +162 -0
- package/dest/modules/validation.d.ts +40 -0
- package/dest/modules/validation.d.ts.map +1 -0
- package/dest/{archiver → modules}/validation.js +41 -17
- package/dest/store/block_store.d.ts +303 -0
- package/dest/store/block_store.d.ts.map +1 -0
- package/dest/store/block_store.js +1206 -0
- package/dest/store/contract_class_store.d.ts +31 -0
- package/dest/store/contract_class_store.d.ts.map +1 -0
- package/dest/store/contract_class_store.js +98 -0
- package/dest/store/contract_instance_store.d.ts +51 -0
- package/dest/store/contract_instance_store.d.ts.map +1 -0
- package/dest/{archiver/kv_archiver_store → store}/contract_instance_store.js +55 -3
- package/dest/store/data_stores.d.ts +68 -0
- package/dest/store/data_stores.d.ts.map +1 -0
- package/dest/store/data_stores.js +54 -0
- package/dest/store/function_names_cache.d.ts +17 -0
- package/dest/store/function_names_cache.d.ts.map +1 -0
- package/dest/store/function_names_cache.js +30 -0
- package/dest/store/l2_tips_cache.d.ts +25 -0
- package/dest/store/l2_tips_cache.d.ts.map +1 -0
- package/dest/store/l2_tips_cache.js +26 -0
- package/dest/store/log_store.d.ts +59 -0
- package/dest/store/log_store.d.ts.map +1 -0
- package/dest/store/log_store.js +310 -0
- package/dest/store/log_store_codec.d.ts +78 -0
- package/dest/store/log_store_codec.d.ts.map +1 -0
- package/dest/store/log_store_codec.js +110 -0
- package/dest/store/message_store.d.ts +50 -0
- package/dest/store/message_store.d.ts.map +1 -0
- package/dest/{archiver/kv_archiver_store → store}/message_store.js +66 -23
- package/dest/{archiver/structs → structs}/data_retrieval.d.ts +1 -1
- package/dest/structs/data_retrieval.d.ts.map +1 -0
- package/dest/structs/inbox_message.d.ts +15 -0
- package/dest/structs/inbox_message.d.ts.map +1 -0
- package/dest/{archiver/structs → structs}/inbox_message.js +6 -6
- package/dest/structs/published.d.ts +2 -0
- package/dest/structs/published.d.ts.map +1 -0
- package/dest/test/fake_l1_state.d.ts +214 -0
- package/dest/test/fake_l1_state.d.ts.map +1 -0
- package/dest/test/fake_l1_state.js +517 -0
- package/dest/test/index.d.ts +2 -1
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +4 -1
- package/dest/test/mock_archiver.d.ts +5 -6
- package/dest/test/mock_archiver.d.ts.map +1 -1
- package/dest/test/mock_archiver.js +8 -12
- package/dest/test/mock_l1_to_l2_message_source.d.ts +6 -7
- package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
- package/dest/test/mock_l1_to_l2_message_source.js +19 -14
- package/dest/test/mock_l2_block_source.d.ts +68 -40
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +347 -145
- package/dest/test/mock_structs.d.ts +83 -4
- package/dest/test/mock_structs.d.ts.map +1 -1
- package/dest/test/mock_structs.js +157 -12
- package/dest/test/noop_l1_archiver.d.ts +34 -0
- package/dest/test/noop_l1_archiver.d.ts.map +1 -0
- package/dest/test/noop_l1_archiver.js +88 -0
- package/package.json +18 -18
- package/src/archiver.ts +785 -0
- package/src/config.ts +125 -0
- package/src/errors.ts +203 -0
- package/src/factory.ts +185 -22
- package/src/index.ts +27 -3
- package/src/interfaces.ts +9 -0
- package/src/l1/README.md +55 -0
- package/src/l1/bin/retrieve-calldata.ts +194 -0
- package/src/l1/calldata_retriever.ts +529 -0
- package/src/{archiver → l1}/data_retrieval.ts +157 -243
- package/src/l1/debug_tx.ts +99 -0
- package/src/l1/spire_proposer.ts +152 -0
- package/src/l1/trace_tx.ts +128 -0
- package/src/l1/types.ts +13 -0
- package/src/l1/validate_historical_logs.ts +140 -0
- package/src/l1/validate_trace.ts +229 -0
- package/src/modules/contract_data_source_adapter.ts +46 -0
- package/src/modules/data_source_base.ts +466 -0
- package/src/modules/data_store_updater.ts +519 -0
- package/src/modules/instrumentation.ts +217 -0
- package/src/modules/l1_synchronizer.ts +1320 -0
- package/src/modules/outbox_trees_resolver.ts +199 -0
- package/src/modules/validation.ts +186 -0
- package/src/store/block_store.ts +1551 -0
- package/src/store/contract_class_store.ts +126 -0
- package/src/{archiver/kv_archiver_store → store}/contract_instance_store.ts +70 -7
- package/src/store/data_stores.ts +104 -0
- package/src/store/function_names_cache.ts +37 -0
- package/src/store/l2_tips_cache.ts +35 -0
- package/src/store/log_store.ts +380 -0
- package/src/store/log_store_codec.ts +143 -0
- package/src/{archiver/kv_archiver_store → store}/message_store.ts +81 -28
- package/src/{archiver/structs → structs}/inbox_message.ts +8 -9
- package/src/{archiver/structs → structs}/published.ts +0 -1
- package/src/test/fake_l1_state.ts +770 -0
- package/src/test/fixtures/debug_traceTransaction-multicall3.json +88 -0
- package/src/test/fixtures/debug_traceTransaction-multiplePropose.json +153 -0
- package/src/test/fixtures/debug_traceTransaction-proxied.json +122 -0
- package/src/test/fixtures/trace_transaction-multicall3.json +65 -0
- package/src/test/fixtures/trace_transaction-multiplePropose.json +319 -0
- package/src/test/fixtures/trace_transaction-proxied.json +128 -0
- package/src/test/fixtures/trace_transaction-randomRevert.json +216 -0
- package/src/test/index.ts +4 -0
- package/src/test/mock_archiver.ts +10 -14
- package/src/test/mock_l1_to_l2_message_source.ts +16 -15
- package/src/test/mock_l2_block_source.ts +416 -161
- package/src/test/mock_structs.ts +292 -14
- package/src/test/noop_l1_archiver.ts +158 -0
- package/dest/archiver/archiver.d.ts +0 -290
- package/dest/archiver/archiver.d.ts.map +0 -1
- package/dest/archiver/archiver.js +0 -1434
- package/dest/archiver/archiver_store.d.ts +0 -256
- package/dest/archiver/archiver_store.d.ts.map +0 -1
- package/dest/archiver/archiver_store.js +0 -4
- package/dest/archiver/archiver_store_test_suite.d.ts +0 -8
- package/dest/archiver/archiver_store_test_suite.d.ts.map +0 -1
- package/dest/archiver/archiver_store_test_suite.js +0 -1289
- package/dest/archiver/config.d.ts +0 -21
- package/dest/archiver/config.d.ts.map +0 -1
- package/dest/archiver/config.js +0 -55
- package/dest/archiver/data_retrieval.d.ts +0 -80
- package/dest/archiver/data_retrieval.d.ts.map +0 -1
- package/dest/archiver/errors.d.ts +0 -12
- package/dest/archiver/errors.d.ts.map +0 -1
- package/dest/archiver/errors.js +0 -17
- package/dest/archiver/index.d.ts +0 -7
- package/dest/archiver/index.d.ts.map +0 -1
- package/dest/archiver/index.js +0 -4
- package/dest/archiver/instrumentation.d.ts +0 -35
- package/dest/archiver/instrumentation.d.ts.map +0 -1
- package/dest/archiver/instrumentation.js +0 -140
- package/dest/archiver/kv_archiver_store/block_store.d.ts +0 -125
- package/dest/archiver/kv_archiver_store/block_store.d.ts.map +0 -1
- package/dest/archiver/kv_archiver_store/block_store.js +0 -371
- package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +0 -18
- package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +0 -1
- package/dest/archiver/kv_archiver_store/contract_class_store.js +0 -120
- package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +0 -24
- package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +0 -1
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +0 -169
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +0 -1
- package/dest/archiver/kv_archiver_store/kv_archiver_store.js +0 -296
- package/dest/archiver/kv_archiver_store/log_store.d.ts +0 -49
- package/dest/archiver/kv_archiver_store/log_store.d.ts.map +0 -1
- package/dest/archiver/kv_archiver_store/log_store.js +0 -337
- package/dest/archiver/kv_archiver_store/message_store.d.ts +0 -39
- package/dest/archiver/kv_archiver_store/message_store.d.ts.map +0 -1
- package/dest/archiver/structs/data_retrieval.d.ts.map +0 -1
- package/dest/archiver/structs/inbox_message.d.ts +0 -15
- package/dest/archiver/structs/inbox_message.d.ts.map +0 -1
- package/dest/archiver/structs/published.d.ts +0 -3
- package/dest/archiver/structs/published.d.ts.map +0 -1
- package/dest/archiver/validation.d.ts +0 -17
- package/dest/archiver/validation.d.ts.map +0 -1
- package/dest/rpc/index.d.ts +0 -9
- package/dest/rpc/index.d.ts.map +0 -1
- package/dest/rpc/index.js +0 -15
- package/src/archiver/archiver.ts +0 -1880
- package/src/archiver/archiver_store.ts +0 -310
- package/src/archiver/archiver_store_test_suite.ts +0 -1295
- package/src/archiver/config.ts +0 -79
- package/src/archiver/errors.ts +0 -26
- package/src/archiver/index.ts +0 -6
- package/src/archiver/instrumentation.ts +0 -187
- package/src/archiver/kv_archiver_store/block_store.ts +0 -482
- package/src/archiver/kv_archiver_store/contract_class_store.ts +0 -176
- package/src/archiver/kv_archiver_store/kv_archiver_store.ts +0 -423
- package/src/archiver/kv_archiver_store/log_store.ts +0 -407
- package/src/archiver/validation.ts +0 -124
- package/src/rpc/index.ts +0 -16
- /package/dest/{archiver/structs → structs}/data_retrieval.js +0 -0
- /package/dest/{archiver/structs → structs}/published.js +0 -0
- /package/src/{archiver/structs → structs}/data_retrieval.ts +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { OutboxContract } from '@aztec/ethereum/contracts';
|
|
2
|
+
import type { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { chunkBy } from '@aztec/foundation/collection';
|
|
4
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
6
|
+
import type { L2Block } from '@aztec/stdlib/block';
|
|
7
|
+
import type { CheckpointData } from '@aztec/stdlib/checkpoint';
|
|
8
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
9
|
+
import { type L2ToL1MembershipWitness, computeL2ToL1MembershipWitness } from '@aztec/stdlib/messaging';
|
|
10
|
+
import type { IndexedTxEffect, TxHash } from '@aztec/stdlib/tx';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Archiver-side view of the data the resolver needs to assemble a witness. The archiver implements
|
|
14
|
+
* each of these natively, so no L2 RPC plumbing is required.
|
|
15
|
+
*/
|
|
16
|
+
export interface OutboxTreesArchiverView {
|
|
17
|
+
getBlocks(query: { epoch: EpochNumber; onlyCheckpointed: true }): Promise<L2Block[]>;
|
|
18
|
+
getBlock(query: { number: BlockNumber }): Promise<L2Block | undefined>;
|
|
19
|
+
getCheckpointsData(query: { epoch: EpochNumber }): Promise<CheckpointData[]>;
|
|
20
|
+
getTxEffect(txHash: TxHash): Promise<IndexedTxEffect | undefined>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A cached per-epoch set of Outbox roots, tagged with the synced L1 block they were read at. */
|
|
24
|
+
type CachedRoots = {
|
|
25
|
+
/** The `MAX_CHECKPOINTS_PER_EPOCH`-length array of partial-proof roots the L1 Outbox holds for the epoch. */
|
|
26
|
+
roots: Fr[];
|
|
27
|
+
/** The node's synced L1 block number at the moment we fetched these roots. */
|
|
28
|
+
fetchedAtL1Block: bigint;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Holds an in-memory cache of per-epoch Outbox roots used to build L2-to-L1 message membership
|
|
33
|
+
* witnesses, plus the single-flight registry that dedupes concurrent fetches.
|
|
34
|
+
*
|
|
35
|
+
* Roots are fetched lazily on request and cached against the node's synced L1 block: a cached entry
|
|
36
|
+
* is reused only while the synced L1 block is unchanged, and re-fetched from L1 once the node
|
|
37
|
+
* advances. The cache is intentionally not persisted — entries are valid for at most one L1 slot, so
|
|
38
|
+
* there is nothing worth carrying across restarts. The four merkle-tree levels are rebuilt per
|
|
39
|
+
* request from the archiver's raw L2 data via `computeL2ToL1MembershipWitness`; no trees or
|
|
40
|
+
* intermediate out-hashes are cached.
|
|
41
|
+
*
|
|
42
|
+
* TODO: Re-add permanent caching for sealed-and-L1-finalized epochs so their roots can be frozen
|
|
43
|
+
* and served without any L1 read once they can no longer change. That logic was dropped here to keep
|
|
44
|
+
* the cache simple; it would likely want a persisted store again.
|
|
45
|
+
*/
|
|
46
|
+
export class OutboxTreesResolver {
|
|
47
|
+
/** In-memory cache of per-epoch Outbox roots, keyed by epoch number. */
|
|
48
|
+
#cache: Map<EpochNumber, CachedRoots> = new Map();
|
|
49
|
+
|
|
50
|
+
/** Single-flight registry for in-flight roots fetches, keyed by epoch number. */
|
|
51
|
+
#pending: Map<EpochNumber, Promise<Fr[] | undefined>> = new Map();
|
|
52
|
+
|
|
53
|
+
/** Adapter exposing the slice of the node API that `computeL2ToL1MembershipWitness` requires. */
|
|
54
|
+
#node: Parameters<typeof computeL2ToL1MembershipWitness>[0];
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly outbox: OutboxContract,
|
|
58
|
+
private readonly archiver: OutboxTreesArchiverView,
|
|
59
|
+
private readonly getSyncedL1BlockNumber: () => Promise<bigint | undefined>,
|
|
60
|
+
private readonly epochDuration: number,
|
|
61
|
+
private readonly log: Logger = createLogger('archiver:outbox'),
|
|
62
|
+
) {
|
|
63
|
+
this.#node = this.makeNodeView();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds the L2-to-L1 membership witness for `message` in tx `txHash`. Returns `undefined` if the
|
|
68
|
+
* tx isn't yet in a block/epoch, or if the Outbox holds no covering partial-proof root for the
|
|
69
|
+
* tx's checkpoint as of the node's synced L1 block — callers should retry in that case.
|
|
70
|
+
*/
|
|
71
|
+
public async getL2ToL1MembershipWitness(
|
|
72
|
+
txHash: TxHash,
|
|
73
|
+
message: Fr,
|
|
74
|
+
messageIndexInTx?: number,
|
|
75
|
+
): Promise<L2ToL1MembershipWitness | undefined> {
|
|
76
|
+
const indexed = await this.archiver.getTxEffect(txHash);
|
|
77
|
+
if (!indexed) {
|
|
78
|
+
this.log.trace(`No tx effect for tx, no witness available`, { txHash });
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Derive the epoch from the tx's slot the same way the node assembles a mined receipt, so the
|
|
83
|
+
// witness helper sees the same view a `getTxReceipt` caller would.
|
|
84
|
+
const epochNumber = getEpochAtSlot(indexed.slotNumber, { epochDuration: this.epochDuration });
|
|
85
|
+
const roots = await this.#getRoots(epochNumber);
|
|
86
|
+
if (roots === undefined) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const receipt = {
|
|
91
|
+
txHash,
|
|
92
|
+
epochNumber,
|
|
93
|
+
blockNumber: indexed.l2BlockNumber,
|
|
94
|
+
txIndexInBlock: indexed.txIndexInBlock,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const syncedBefore = await this.getSyncedL1BlockNumber();
|
|
98
|
+
try {
|
|
99
|
+
return await computeL2ToL1MembershipWitness(this.#node, roots, message, receipt, messageIndexInTx);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// The helper throws if the locally-assembled epoch root disagrees with the cached Outbox root.
|
|
102
|
+
// The cached roots are pinned to the node's synced L1 block, but the helper reads block /
|
|
103
|
+
// checkpoint data live, so if the archiver advanced mid-assembly the two can momentarily reflect
|
|
104
|
+
// different heights and produce a transient mismatch. When the synced block moved during
|
|
105
|
+
// assembly we treat the mismatch as transient and return undefined so the caller can retry; a
|
|
106
|
+
// mismatch at a stable synced block is a genuine node/L1 disagreement and must surface.
|
|
107
|
+
const syncedAfter = await this.getSyncedL1BlockNumber();
|
|
108
|
+
if (syncedBefore !== syncedAfter && err instanceof Error && err.message.includes('does not match Outbox')) {
|
|
109
|
+
this.log.debug(`Transient outbox root mismatch during sync, returning no witness`, {
|
|
110
|
+
txHash,
|
|
111
|
+
epoch: epochNumber,
|
|
112
|
+
});
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Lazily returns the per-epoch Outbox roots, reusing the cached entry while the node's synced L1
|
|
121
|
+
* block is unchanged and re-fetching from L1 once it advances. Returns `undefined` if the node has
|
|
122
|
+
* not established a synced L1 block yet. Concurrent requests for the same epoch share a single
|
|
123
|
+
* in-flight fetch via {@link #pending}.
|
|
124
|
+
*/
|
|
125
|
+
#getRoots(epoch: EpochNumber): Promise<Fr[] | undefined> {
|
|
126
|
+
const inFlight = this.#pending.get(epoch);
|
|
127
|
+
if (inFlight) {
|
|
128
|
+
return inFlight;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const promise = this.#refreshRoots(epoch).finally(() => {
|
|
132
|
+
this.#pending.delete(epoch);
|
|
133
|
+
});
|
|
134
|
+
this.#pending.set(epoch, promise);
|
|
135
|
+
return promise;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Single-flight body for {@link #getRoots}. Re-reads the cached entry and synced block here —
|
|
140
|
+
* rather than capturing them before winning the {@link #pending} slot — so a late entrant that
|
|
141
|
+
* missed the slot cannot overwrite a fresher entry the previous winner just wrote.
|
|
142
|
+
*/
|
|
143
|
+
async #refreshRoots(epoch: EpochNumber): Promise<Fr[] | undefined> {
|
|
144
|
+
const synced = await this.getSyncedL1BlockNumber();
|
|
145
|
+
if (synced === undefined) {
|
|
146
|
+
// The archiver has no synced L1 block yet, so there is no consistent height to read the Outbox
|
|
147
|
+
// at. The caller retries once the node has synced.
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cached = this.#cache.get(epoch);
|
|
152
|
+
if (cached && cached.fetchedAtL1Block === synced) {
|
|
153
|
+
return cached.roots;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// TODO: Re-add permanent caching for sealed-and-L1-finalized epochs so we can skip the L1 read
|
|
157
|
+
// entirely once an epoch's roots can no longer change. For now the cache is valid only for the
|
|
158
|
+
// current synced L1 block and we re-query the Outbox whenever the node advances.
|
|
159
|
+
const roots = await this.outbox.getRoots(epoch, { blockNumber: synced });
|
|
160
|
+
this.#cache.set(epoch, { roots, fetchedAtL1Block: synced });
|
|
161
|
+
this.log.trace(`Fetched outbox roots for epoch ${epoch}`, {
|
|
162
|
+
epoch,
|
|
163
|
+
syncedL1Block: synced,
|
|
164
|
+
nonZeroRoots: roots.filter(r => !r.isZero()).length,
|
|
165
|
+
});
|
|
166
|
+
return roots;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Returns the L2-to-L1 messages in `epoch`, organized as `checkpoint[] → block[] → tx[] →
|
|
171
|
+
* message[]`. Mirrors `AztecNodeService.getL2ToL1Messages` so the resolver can run off the
|
|
172
|
+
* archiver directly without going through the node RPC surface.
|
|
173
|
+
*/
|
|
174
|
+
async #getL2ToL1Messages(epoch: EpochNumber): Promise<Fr[][][][]> {
|
|
175
|
+
const blocks = await this.archiver.getBlocks({ epoch, onlyCheckpointed: true });
|
|
176
|
+
const blocksInCheckpoints = chunkBy(blocks, block => block.header.globalVariables.slotNumber);
|
|
177
|
+
return blocksInCheckpoints.map(slotBlocks =>
|
|
178
|
+
slotBlocks.map(block => block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Builds the node-shaped adapter handed to `computeL2ToL1MembershipWitness`. The helper invokes
|
|
184
|
+
* `getL2ToL1Messages` / `getBlock` / `getCheckpointsData`; `getTxReceipt` is part of the helper's
|
|
185
|
+
* node type but is never invoked because we always pass an already-resolved receipt.
|
|
186
|
+
*/
|
|
187
|
+
private makeNodeView(): Parameters<typeof computeL2ToL1MembershipWitness>[0] {
|
|
188
|
+
return {
|
|
189
|
+
getL2ToL1Messages: (epoch: EpochNumber) => this.#getL2ToL1Messages(epoch),
|
|
190
|
+
getBlock: ((param: BlockNumber) => this.archiver.getBlock({ number: param })) as Parameters<
|
|
191
|
+
typeof computeL2ToL1MembershipWitness
|
|
192
|
+
>[0]['getBlock'],
|
|
193
|
+
getCheckpointsData: (query: { epoch: EpochNumber }) => this.archiver.getCheckpointsData(query),
|
|
194
|
+
getTxReceipt: (() => {
|
|
195
|
+
throw new Error('OutboxTreesResolver always passes a resolved receipt; getTxReceipt must not be called');
|
|
196
|
+
}) as Parameters<typeof computeL2ToL1MembershipWitness>[0]['getTxReceipt'],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import type { ViemCommitteeAttestations } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { type CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { compactArray } from '@aztec/foundation/collection';
|
|
5
|
+
import type { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
|
+
import type { Logger } from '@aztec/foundation/log';
|
|
7
|
+
import {
|
|
8
|
+
type AttestationInfo,
|
|
9
|
+
type CommitteeAttestation,
|
|
10
|
+
type ValidateCheckpointNegativeResult,
|
|
11
|
+
type ValidateCheckpointResult,
|
|
12
|
+
getAttestationInfoFromPayload,
|
|
13
|
+
} from '@aztec/stdlib/block';
|
|
14
|
+
import type { CheckpointInfo, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
15
|
+
import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
16
|
+
import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
17
|
+
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
18
|
+
|
|
19
|
+
export type { ValidateCheckpointResult };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extracts attestation information from a published checkpoint.
|
|
23
|
+
* Returns info for each attestation, preserving array indices.
|
|
24
|
+
*/
|
|
25
|
+
export function getAttestationInfoFromPublishedCheckpoint(
|
|
26
|
+
{ checkpoint, attestations }: PublishedCheckpoint,
|
|
27
|
+
signatureContext: CoordinationSignatureContext,
|
|
28
|
+
): AttestationInfo[] {
|
|
29
|
+
const payload = ConsensusPayload.fromCheckpoint(checkpoint, signatureContext);
|
|
30
|
+
return getAttestationInfoFromPayload(payload, attestations);
|
|
31
|
+
}
|
|
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
|
+
|
|
44
|
+
/**
|
|
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.
|
|
49
|
+
*/
|
|
50
|
+
export function validateCheckpointAttestationsFromCalldata(
|
|
51
|
+
checkpoint: CalldataCheckpointForAttestations,
|
|
52
|
+
epochCache: EpochCache,
|
|
53
|
+
constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
54
|
+
signatureContext: CoordinationSignatureContext,
|
|
55
|
+
logger?: Logger,
|
|
56
|
+
): Promise<ValidateCheckpointResult> {
|
|
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);
|
|
96
|
+
const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
|
|
97
|
+
const headerHash = payload.header.hash();
|
|
98
|
+
const archiveRoot = payload.archive.toString();
|
|
99
|
+
const slot = payload.header.slotNumber;
|
|
100
|
+
const checkpointNumber = checkpointInfo.checkpointNumber;
|
|
101
|
+
const epoch: EpochNumber = getEpochAtSlot(slot, constants);
|
|
102
|
+
const { committee, seed } = await epochCache.getCommitteeForEpoch(epoch);
|
|
103
|
+
const logData = { checkpointNumber, slot, epoch, headerHash, archiveRoot };
|
|
104
|
+
|
|
105
|
+
logger?.debug(`Validating attestations for checkpoint ${checkpointNumber} at slot ${slot} in epoch ${epoch}`, {
|
|
106
|
+
committee: (committee ?? []).map(member => member.toString()),
|
|
107
|
+
recoveredAttestors: attestorInfos,
|
|
108
|
+
postedAttestations: attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
|
|
109
|
+
...logData,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!committee || committee.length === 0) {
|
|
113
|
+
logger?.warn(
|
|
114
|
+
`No committee found for epoch ${epoch} at slot ${slot}. Accepting checkpoint without validation.`,
|
|
115
|
+
logData,
|
|
116
|
+
);
|
|
117
|
+
return { valid: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (await epochCache.isEscapeHatchOpen(epoch)) {
|
|
121
|
+
logger?.warn(`Escape hatch open for epoch ${epoch} at slot ${slot}, skipping checkpoint validation`);
|
|
122
|
+
return { valid: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const requiredAttestationCount = computeQuorum(committee.length);
|
|
126
|
+
|
|
127
|
+
const failedValidationResult = <TReason extends ValidateCheckpointNegativeResult['reason']>(reason: TReason) => ({
|
|
128
|
+
valid: false as const,
|
|
129
|
+
reason,
|
|
130
|
+
checkpoint: checkpointInfo,
|
|
131
|
+
committee,
|
|
132
|
+
seed,
|
|
133
|
+
epoch,
|
|
134
|
+
attestors,
|
|
135
|
+
attestations,
|
|
136
|
+
verbatimAttestations,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < attestorInfos.length; i++) {
|
|
140
|
+
const info = attestorInfos[i];
|
|
141
|
+
|
|
142
|
+
// Fail on invalid signatures (no address recovered)
|
|
143
|
+
if (info.status === 'invalid-signature' || info.status === 'empty') {
|
|
144
|
+
logger?.warn(`Attestation with empty or invalid signature at slot ${slot}`, {
|
|
145
|
+
committee,
|
|
146
|
+
invalidIndex: i,
|
|
147
|
+
...logData,
|
|
148
|
+
});
|
|
149
|
+
return { ...failedValidationResult('invalid-attestation'), invalidIndex: i };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if the attestor at this index matches the committee member at the same index
|
|
153
|
+
if (info.status === 'recovered-from-signature' || info.status === 'provided-as-address') {
|
|
154
|
+
const signer = info.address.toString();
|
|
155
|
+
const expectedCommitteeMember = committee[i]?.toString();
|
|
156
|
+
|
|
157
|
+
if (!expectedCommitteeMember || signer !== expectedCommitteeMember) {
|
|
158
|
+
logger?.warn(
|
|
159
|
+
`Attestation at index ${i} from ${signer} does not match expected committee member ${expectedCommitteeMember} at slot ${slot}`,
|
|
160
|
+
{
|
|
161
|
+
committee,
|
|
162
|
+
invalidIndex: i,
|
|
163
|
+
...logData,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
return { ...failedValidationResult('invalid-attestation'), invalidIndex: i };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const validAttestationCount = attestorInfos.filter(info => info.status === 'recovered-from-signature').length;
|
|
172
|
+
if (validAttestationCount < requiredAttestationCount) {
|
|
173
|
+
logger?.warn(`Insufficient attestations for checkpoint at slot ${slot}`, {
|
|
174
|
+
requiredAttestations: requiredAttestationCount,
|
|
175
|
+
actualAttestations: validAttestationCount,
|
|
176
|
+
...logData,
|
|
177
|
+
});
|
|
178
|
+
return failedValidationResult('insufficient-attestations');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
logger?.debug(
|
|
182
|
+
`Checkpoint attestations validated successfully for checkpoint ${checkpointNumber} at slot ${slot}`,
|
|
183
|
+
logData,
|
|
184
|
+
);
|
|
185
|
+
return { valid: true };
|
|
186
|
+
}
|