@aztec/archiver 0.0.1-commit.7b97ef96e → 0.0.1-commit.7cbc774
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 +19 -11
- package/dest/archiver.d.ts +36 -17
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +257 -75
- package/dest/config.d.ts +6 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -15
- package/dest/errors.d.ts +55 -9
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +81 -14
- package/dest/factory.d.ts +13 -9
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +48 -39
- package/dest/index.d.ts +11 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +10 -2
- package/dest/l1/bin/retrieve-calldata.js +32 -28
- package/dest/l1/calldata_retriever.d.ts +71 -53
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +190 -262
- package/dest/l1/data_retrieval.d.ts +24 -16
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +39 -45
- package/dest/l1/spire_proposer.d.ts +5 -5
- package/dest/l1/spire_proposer.d.ts.map +1 -1
- package/dest/l1/spire_proposer.js +9 -17
- package/dest/l1/trace_tx.d.ts +12 -66
- package/dest/l1/trace_tx.d.ts.map +1 -1
- 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/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 +40 -0
- package/dest/modules/data_source_base.d.ts +70 -46
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +270 -135
- package/dest/modules/data_store_updater.d.ts +42 -17
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +191 -122
- package/dest/modules/instrumentation.d.ts +18 -2
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +35 -7
- package/dest/modules/l1_synchronizer.d.ts +12 -11
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +439 -207
- package/dest/modules/validation.d.ts +4 -3
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +6 -6
- package/dest/store/block_store.d.ts +174 -70
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +696 -250
- package/dest/store/contract_class_store.d.ts +17 -4
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +24 -68
- package/dest/store/contract_instance_store.d.ts +28 -1
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +37 -2
- 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 +13 -7
- package/dest/store/l2_tips_cache.d.ts.map +1 -1
- package/dest/store/l2_tips_cache.js +13 -76
- package/dest/store/log_store.d.ts +42 -37
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +262 -408
- package/dest/store/log_store_codec.d.ts +70 -0
- package/dest/store/log_store_codec.d.ts.map +1 -0
- package/dest/store/log_store_codec.js +101 -0
- package/dest/store/message_store.d.ts +11 -1
- package/dest/store/message_store.d.ts.map +1 -1
- package/dest/store/message_store.js +51 -9
- package/dest/test/fake_l1_state.d.ts +22 -1
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +152 -24
- 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 +2 -1
- package/dest/test/mock_l2_block_source.d.ts +52 -46
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +246 -170
- package/dest/test/mock_structs.d.ts +4 -1
- package/dest/test/mock_structs.d.ts.map +1 -1
- package/dest/test/mock_structs.js +13 -1
- package/dest/test/noop_l1_archiver.d.ts +12 -6
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +26 -9
- package/package.json +14 -14
- package/src/archiver.ts +313 -75
- package/src/config.ts +32 -12
- package/src/errors.ts +122 -21
- package/src/factory.ts +54 -30
- package/src/index.ts +18 -2
- package/src/l1/README.md +25 -68
- package/src/l1/bin/retrieve-calldata.ts +40 -27
- package/src/l1/calldata_retriever.ts +243 -384
- package/src/l1/data_retrieval.ts +55 -69
- package/src/l1/spire_proposer.ts +7 -15
- package/src/l1/validate_historical_logs.ts +140 -0
- package/src/modules/contract_data_source_adapter.ts +55 -0
- package/src/modules/data_source_base.ts +336 -171
- package/src/modules/data_store_updater.ts +224 -154
- package/src/modules/instrumentation.ts +48 -8
- package/src/modules/l1_synchronizer.ts +579 -254
- package/src/modules/validation.ts +10 -9
- package/src/store/block_store.ts +865 -290
- package/src/store/contract_class_store.ts +31 -103
- package/src/store/contract_instance_store.ts +51 -5
- package/src/store/data_stores.ts +104 -0
- package/src/store/function_names_cache.ts +37 -0
- package/src/store/l2_tips_cache.ts +16 -70
- package/src/store/log_store.ts +301 -559
- package/src/store/log_store_codec.ts +132 -0
- package/src/store/message_store.ts +60 -10
- package/src/structs/inbox_message.ts +1 -1
- package/src/test/fake_l1_state.ts +198 -35
- package/src/test/mock_l1_to_l2_message_source.ts +1 -0
- package/src/test/mock_l2_block_source.ts +309 -205
- package/src/test/mock_structs.ts +20 -6
- package/src/test/noop_l1_archiver.ts +39 -9
- package/dest/store/kv_archiver_store.d.ts +0 -354
- package/dest/store/kv_archiver_store.d.ts.map +0 -1
- package/dest/store/kv_archiver_store.js +0 -464
- package/src/store/kv_archiver_store.ts +0 -671
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum/contracts';
|
|
2
|
+
import { LruSet } from '@aztec/foundation/collection';
|
|
2
3
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
4
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
-
import {
|
|
5
|
+
import { RollupAbi } from '@aztec/l1-artifacts';
|
|
5
6
|
import { CommitteeAttestation } from '@aztec/stdlib/block';
|
|
6
|
-
import {
|
|
7
|
+
import { computeCheckpointPayloadDigest } from '@aztec/stdlib/checkpoint';
|
|
7
8
|
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
8
9
|
import { decodeFunctionData, encodeAbiParameters, hexToBytes, keccak256, multicall3Abi, toFunctionSelector } from 'viem';
|
|
9
10
|
import { getSuccessfulCallsFromDebug } from './debug_tx.js';
|
|
10
|
-
import {
|
|
11
|
+
import { getCallsFromSpireProposer } from './spire_proposer.js';
|
|
11
12
|
import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
12
13
|
/**
|
|
13
14
|
* Extracts calldata to the `propose` method of the rollup contract from an L1 transaction
|
|
14
|
-
* in order to reconstruct an L2 block header.
|
|
15
|
+
* in order to reconstruct an L2 block header. Uses hash matching against expected hashes
|
|
16
|
+
* from the CheckpointProposed event to verify the correct propose calldata.
|
|
15
17
|
*/ export class CalldataRetriever {
|
|
16
18
|
publicClient;
|
|
17
19
|
debugClient;
|
|
18
20
|
targetCommitteeSize;
|
|
19
21
|
instrumentation;
|
|
20
22
|
logger;
|
|
21
|
-
|
|
23
|
+
rollupAddress;
|
|
24
|
+
/** Tx hashes we've already logged for trace+debug failure (log once per tx per process). */ static traceFailureWarnedTxHashes = new LruSet(1000);
|
|
22
25
|
/** Clears the trace-failure warned set. For testing only. */ static resetTraceFailureWarnedForTesting() {
|
|
23
26
|
CalldataRetriever.traceFailureWarnedTxHashes.clear();
|
|
24
27
|
}
|
|
25
|
-
|
|
26
|
-
rollupAddress;
|
|
27
|
-
constructor(publicClient, debugClient, targetCommitteeSize, instrumentation, logger, contractAddresses){
|
|
28
|
+
constructor(publicClient, debugClient, targetCommitteeSize, instrumentation, logger, rollupAddress){
|
|
28
29
|
this.publicClient = publicClient;
|
|
29
30
|
this.debugClient = debugClient;
|
|
30
31
|
this.targetCommitteeSize = targetCommitteeSize;
|
|
31
32
|
this.instrumentation = instrumentation;
|
|
32
33
|
this.logger = logger;
|
|
33
|
-
this.rollupAddress =
|
|
34
|
-
|
|
34
|
+
this.rollupAddress = rollupAddress;
|
|
35
|
+
}
|
|
36
|
+
getSignatureContext() {
|
|
37
|
+
return {
|
|
38
|
+
chainId: this.publicClient.chain.id,
|
|
39
|
+
rollupAddress: this.rollupAddress
|
|
40
|
+
};
|
|
35
41
|
}
|
|
36
42
|
/**
|
|
37
43
|
* Gets checkpoint header and metadata from the calldata of an L1 transaction.
|
|
@@ -39,84 +45,94 @@ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
|
39
45
|
* @param txHash - Hash of the tx that published it.
|
|
40
46
|
* @param blobHashes - Blob hashes for the checkpoint.
|
|
41
47
|
* @param checkpointNumber - Checkpoint number.
|
|
42
|
-
* @param expectedHashes -
|
|
48
|
+
* @param expectedHashes - Expected hashes from the CheckpointProposed event for validation
|
|
43
49
|
* @returns Checkpoint header and metadata from the calldata, deserialized
|
|
44
50
|
*/ async getCheckpointFromRollupTx(txHash, _blobHashes, checkpointNumber, expectedHashes) {
|
|
45
|
-
this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}
|
|
46
|
-
willValidateHashes: !!expectedHashes.attestationsHash || !!expectedHashes.payloadDigest,
|
|
47
|
-
hasAttestationsHash: !!expectedHashes.attestationsHash,
|
|
48
|
-
hasPayloadDigest: !!expectedHashes.payloadDigest
|
|
49
|
-
});
|
|
51
|
+
this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`);
|
|
50
52
|
const tx = await this.publicClient.getTransaction({
|
|
51
53
|
hash: txHash
|
|
52
54
|
});
|
|
53
|
-
|
|
54
|
-
return this.decodeAndBuildCheckpoint(proposeCalldata, tx.blockHash, checkpointNumber, expectedHashes);
|
|
55
|
+
return this.getCheckpointFromTx(tx, checkpointNumber, expectedHashes);
|
|
55
56
|
}
|
|
56
|
-
/** Gets
|
|
57
|
-
// Try to decode as multicall3 with
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
57
|
+
/** Gets checkpoint data from a transaction by trying decode strategies then falling back to trace. */ async getCheckpointFromTx(tx, checkpointNumber, expectedHashes) {
|
|
58
|
+
// Try to decode as multicall3 with hash-verified matching
|
|
59
|
+
const multicall3Result = this.tryDecodeMulticall3(tx, expectedHashes, checkpointNumber, tx.blockHash);
|
|
60
|
+
if (multicall3Result) {
|
|
60
61
|
this.logger.trace(`Decoded propose calldata from multicall3 for tx ${tx.hash}`);
|
|
61
62
|
this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
|
|
62
|
-
return
|
|
63
|
+
return multicall3Result;
|
|
63
64
|
}
|
|
64
65
|
// Try to decode as direct propose call
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
66
|
+
const directResult = this.tryDecodeDirectPropose(tx, expectedHashes, checkpointNumber, tx.blockHash);
|
|
67
|
+
if (directResult) {
|
|
67
68
|
this.logger.trace(`Decoded propose calldata from direct call for tx ${tx.hash}`);
|
|
68
69
|
this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
|
|
69
|
-
return
|
|
70
|
+
return directResult;
|
|
70
71
|
}
|
|
71
72
|
// Try to decode as Spire Proposer multicall wrapper
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
73
|
+
const spireResult = await this.tryDecodeSpireProposer(tx, expectedHashes, checkpointNumber, tx.blockHash);
|
|
74
|
+
if (spireResult) {
|
|
74
75
|
this.logger.trace(`Decoded propose calldata from Spire Proposer for tx ${tx.hash}`);
|
|
75
76
|
this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
|
|
76
|
-
return
|
|
77
|
+
return spireResult;
|
|
77
78
|
}
|
|
78
79
|
// Fall back to trace-based extraction
|
|
79
80
|
this.logger.warn(`Failed to decode multicall3, direct propose, or Spire proposer for L1 tx ${tx.hash}, falling back to trace for checkpoint ${checkpointNumber}`);
|
|
80
81
|
this.instrumentation?.recordBlockProposalTxTarget(tx.to ?? EthAddress.ZERO.toString(), true);
|
|
81
|
-
|
|
82
|
+
const tracedCalldata = await this.extractCalldataViaTrace(tx.hash);
|
|
83
|
+
const tracedResult = this.tryDecodeAndVerifyPropose(tracedCalldata, expectedHashes, checkpointNumber, tx.blockHash);
|
|
84
|
+
if (!tracedResult) {
|
|
85
|
+
throw new Error(`Hash mismatch for traced propose calldata in tx ${tx.hash} for checkpoint ${checkpointNumber}`);
|
|
86
|
+
}
|
|
87
|
+
return tracedResult;
|
|
82
88
|
}
|
|
83
89
|
/**
|
|
84
90
|
* Attempts to decode a transaction as a Spire Proposer multicall wrapper.
|
|
85
|
-
* If successful,
|
|
91
|
+
* If successful, iterates all wrapped calls and validates each as either multicall3
|
|
92
|
+
* or direct propose, verifying against expected hashes.
|
|
86
93
|
* @param tx - The transaction to decode
|
|
87
|
-
* @
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
* @param expectedHashes - Expected hashes for hash-verified matching
|
|
95
|
+
* @param checkpointNumber - The checkpoint number
|
|
96
|
+
* @param blockHash - The L1 block hash
|
|
97
|
+
* @returns The checkpoint data if successfully decoded and validated, undefined otherwise
|
|
98
|
+
*/ async tryDecodeSpireProposer(tx, expectedHashes, checkpointNumber, blockHash) {
|
|
99
|
+
// Try to decode as Spire Proposer multicall (extracts all wrapped calls)
|
|
100
|
+
const spireWrappedCalls = await getCallsFromSpireProposer(tx, this.publicClient, this.logger);
|
|
101
|
+
if (!spireWrappedCalls) {
|
|
92
102
|
return undefined;
|
|
93
103
|
}
|
|
94
|
-
this.logger.trace(`Decoded Spire Proposer wrapping for tx ${tx.hash},
|
|
95
|
-
//
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
this.logger.trace(`Decoded Spire Proposer wrapping for tx ${tx.hash}, ${spireWrappedCalls.length} inner call(s)`);
|
|
105
|
+
// Try each wrapped call as either multicall3 or direct propose
|
|
106
|
+
for (const spireWrappedCall of spireWrappedCalls){
|
|
107
|
+
const wrappedTx = {
|
|
108
|
+
to: spireWrappedCall.to,
|
|
109
|
+
input: spireWrappedCall.data,
|
|
110
|
+
hash: tx.hash
|
|
111
|
+
};
|
|
112
|
+
const multicall3Result = this.tryDecodeMulticall3(wrappedTx, expectedHashes, checkpointNumber, blockHash);
|
|
113
|
+
if (multicall3Result) {
|
|
114
|
+
this.logger.trace(`Decoded propose calldata from Spire Proposer to multicall3 for tx ${tx.hash}`);
|
|
115
|
+
return multicall3Result;
|
|
116
|
+
}
|
|
117
|
+
const directResult = this.tryDecodeDirectPropose(wrappedTx, expectedHashes, checkpointNumber, blockHash);
|
|
118
|
+
if (directResult) {
|
|
119
|
+
this.logger.trace(`Decoded propose calldata from Spire Proposer to direct propose for tx ${tx.hash}`);
|
|
120
|
+
return directResult;
|
|
121
|
+
}
|
|
110
122
|
}
|
|
111
|
-
this.logger.warn(`Spire Proposer wrapped
|
|
123
|
+
this.logger.warn(`Spire Proposer wrapped calls could not be decoded as multicall3 or direct propose for tx ${tx.hash}`);
|
|
112
124
|
return undefined;
|
|
113
125
|
}
|
|
114
126
|
/**
|
|
115
127
|
* Attempts to decode transaction input as multicall3 and extract propose calldata.
|
|
116
|
-
*
|
|
128
|
+
* Finds all calls matching the rollup address and propose selector, then decodes
|
|
129
|
+
* and verifies each candidate against expected hashes from the CheckpointProposed event.
|
|
117
130
|
* @param tx - The transaction-like object with to, input, and hash
|
|
118
|
-
* @
|
|
119
|
-
|
|
131
|
+
* @param expectedHashes - Expected hashes from CheckpointProposed event
|
|
132
|
+
* @param checkpointNumber - The checkpoint number
|
|
133
|
+
* @param blockHash - The L1 block hash
|
|
134
|
+
* @returns The checkpoint data if successfully validated, undefined otherwise
|
|
135
|
+
*/ tryDecodeMulticall3(tx, expectedHashes, checkpointNumber, blockHash) {
|
|
120
136
|
const txHash = tx.hash;
|
|
121
137
|
try {
|
|
122
138
|
// Check if transaction is to Multicall3 address
|
|
@@ -146,59 +162,51 @@ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
|
146
162
|
return undefined;
|
|
147
163
|
}
|
|
148
164
|
const [calls] = multicall3Args;
|
|
149
|
-
//
|
|
165
|
+
// Find all calls matching rollup address + propose selector
|
|
150
166
|
const rollupAddressLower = this.rollupAddress.toString().toLowerCase();
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
167
|
+
const proposeSelectorLower = PROPOSE_SELECTOR.toLowerCase();
|
|
168
|
+
const candidates = [];
|
|
169
|
+
for (const call of calls){
|
|
170
|
+
const addr = call.target.toLowerCase();
|
|
171
|
+
const callData = call.callData;
|
|
156
172
|
if (callData.length < 10) {
|
|
157
|
-
|
|
158
|
-
this.logger.warn(`Invalid calldata length at index ${i} (${callData.length})`, {
|
|
159
|
-
txHash
|
|
160
|
-
});
|
|
161
|
-
return undefined;
|
|
173
|
+
continue;
|
|
162
174
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!validCall) {
|
|
167
|
-
this.logger.warn(`Invalid contract call detected in multicall3`, {
|
|
168
|
-
index: i,
|
|
169
|
-
targetAddress: addr,
|
|
170
|
-
functionSelector,
|
|
171
|
-
validCalls: this.validContractCalls.map((c)=>({
|
|
172
|
-
address: c.address,
|
|
173
|
-
selector: c.functionSelector
|
|
174
|
-
})),
|
|
175
|
-
txHash
|
|
176
|
-
});
|
|
177
|
-
return undefined;
|
|
175
|
+
const selector = callData.slice(0, 10).toLowerCase();
|
|
176
|
+
if (addr === rollupAddressLower && selector === proposeSelectorLower) {
|
|
177
|
+
candidates.push(callData);
|
|
178
178
|
}
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
}
|
|
180
|
+
if (candidates.length === 0) {
|
|
181
|
+
this.logger.debug(`No propose candidates found in multicall3`, {
|
|
182
|
+
txHash
|
|
181
183
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
// Decode, verify, and build for each candidate
|
|
187
|
+
const verified = [];
|
|
188
|
+
for (const candidate of candidates){
|
|
189
|
+
const result = this.tryDecodeAndVerifyPropose(candidate, expectedHashes, checkpointNumber, blockHash);
|
|
190
|
+
if (result) {
|
|
191
|
+
verified.push(result);
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.logger.warn(`No propose calls found in multicall3`, {
|
|
194
|
+
if (verified.length === 1) {
|
|
195
|
+
this.logger.trace(`Verified single propose candidate via hash matching`, {
|
|
190
196
|
txHash
|
|
191
197
|
});
|
|
192
|
-
return
|
|
198
|
+
return verified[0];
|
|
193
199
|
}
|
|
194
|
-
if (
|
|
195
|
-
this.logger.warn(`Multiple propose
|
|
200
|
+
if (verified.length > 1) {
|
|
201
|
+
this.logger.warn(`Multiple propose candidates verified (${verified.length}), returning first (identical data)`, {
|
|
196
202
|
txHash
|
|
197
203
|
});
|
|
198
|
-
return
|
|
204
|
+
return verified[0];
|
|
199
205
|
}
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
this.logger.debug(`No candidates verified against expected hashes`, {
|
|
207
|
+
txHash
|
|
208
|
+
});
|
|
209
|
+
return undefined;
|
|
202
210
|
} catch (err) {
|
|
203
211
|
// Any decoding error triggers fallback to trace
|
|
204
212
|
this.logger.warn(`Failed to decode multicall3: ${err}`, {
|
|
@@ -209,10 +217,13 @@ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
|
209
217
|
}
|
|
210
218
|
/**
|
|
211
219
|
* Attempts to decode transaction as a direct propose call to the rollup contract.
|
|
212
|
-
*
|
|
220
|
+
* Decodes, verifies hashes, and builds checkpoint data in a single pass.
|
|
213
221
|
* @param tx - The transaction-like object with to, input, and hash
|
|
214
|
-
* @
|
|
215
|
-
|
|
222
|
+
* @param expectedHashes - Expected hashes from CheckpointProposed event
|
|
223
|
+
* @param checkpointNumber - The checkpoint number
|
|
224
|
+
* @param blockHash - The L1 block hash
|
|
225
|
+
* @returns The checkpoint data if successfully validated, undefined otherwise
|
|
226
|
+
*/ tryDecodeDirectPropose(tx, expectedHashes, checkpointNumber, blockHash) {
|
|
216
227
|
const txHash = tx.hash;
|
|
217
228
|
try {
|
|
218
229
|
// Check if transaction is to the rollup address
|
|
@@ -222,23 +233,22 @@ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
|
222
233
|
});
|
|
223
234
|
return undefined;
|
|
224
235
|
}
|
|
225
|
-
//
|
|
236
|
+
// Validate it's a propose call before full decode+verify
|
|
226
237
|
const { functionName } = decodeFunctionData({
|
|
227
238
|
abi: RollupAbi,
|
|
228
239
|
data: tx.input
|
|
229
240
|
});
|
|
230
|
-
// If not propose, return undefined
|
|
231
241
|
if (functionName !== 'propose') {
|
|
232
242
|
this.logger.warn(`Transaction to rollup is not propose (got ${functionName})`, {
|
|
233
243
|
txHash
|
|
234
244
|
});
|
|
235
245
|
return undefined;
|
|
236
246
|
}
|
|
237
|
-
//
|
|
247
|
+
// Decode, verify hashes, and build checkpoint data
|
|
238
248
|
this.logger.trace(`Validated direct propose call to rollup`, {
|
|
239
249
|
txHash
|
|
240
250
|
});
|
|
241
|
-
return tx.input;
|
|
251
|
+
return this.tryDecodeAndVerifyPropose(tx.input, expectedHashes, checkpointNumber, blockHash);
|
|
242
252
|
} catch (err) {
|
|
243
253
|
// Any decoding error means it's not a valid propose call
|
|
244
254
|
this.logger.warn(`Failed to decode as direct propose: ${err}`, {
|
|
@@ -300,9 +310,84 @@ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
|
300
310
|
return calls[0].input;
|
|
301
311
|
}
|
|
302
312
|
/**
|
|
313
|
+
* Decodes propose calldata, verifies against expected hashes, and builds checkpoint data.
|
|
314
|
+
* Returns undefined on decode errors or hash mismatches (soft failure for try-based callers).
|
|
315
|
+
* @param proposeCalldata - The propose function calldata
|
|
316
|
+
* @param expectedHashes - Expected hashes from the CheckpointProposed event
|
|
317
|
+
* @param checkpointNumber - The checkpoint number
|
|
318
|
+
* @param blockHash - The L1 block hash
|
|
319
|
+
* @returns The decoded checkpoint data, or undefined on failure
|
|
320
|
+
*/ tryDecodeAndVerifyPropose(proposeCalldata, expectedHashes, checkpointNumber, blockHash) {
|
|
321
|
+
try {
|
|
322
|
+
const { functionName, args } = decodeFunctionData({
|
|
323
|
+
abi: RollupAbi,
|
|
324
|
+
data: proposeCalldata
|
|
325
|
+
});
|
|
326
|
+
if (functionName !== 'propose') {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
const [decodedArgs, packedAttestations] = args;
|
|
330
|
+
// Verify attestationsHash
|
|
331
|
+
const computedAttestationsHash = this.computeAttestationsHash(packedAttestations);
|
|
332
|
+
if (!Buffer.from(hexToBytes(computedAttestationsHash)).equals(Buffer.from(hexToBytes(expectedHashes.attestationsHash)))) {
|
|
333
|
+
this.logger.warn(`Attestations hash mismatch during verification`, {
|
|
334
|
+
computed: computedAttestationsHash,
|
|
335
|
+
expected: expectedHashes.attestationsHash
|
|
336
|
+
});
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
// Verify payloadDigest
|
|
340
|
+
const header = CheckpointHeader.fromViem(decodedArgs.header);
|
|
341
|
+
const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
|
|
342
|
+
const feeAssetPriceModifier = decodedArgs.oracleInput.feeAssetPriceModifier;
|
|
343
|
+
const computedPayloadDigest = this.computePayloadDigest(header, archiveRoot, feeAssetPriceModifier);
|
|
344
|
+
if (!Buffer.from(hexToBytes(computedPayloadDigest)).equals(Buffer.from(hexToBytes(expectedHashes.payloadDigest)))) {
|
|
345
|
+
this.logger.warn(`Payload digest mismatch during verification`, {
|
|
346
|
+
computed: computedPayloadDigest,
|
|
347
|
+
expected: expectedHashes.payloadDigest
|
|
348
|
+
});
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
|
|
352
|
+
this.logger.trace(`Validated and decoded propose calldata for checkpoint ${checkpointNumber}`, {
|
|
353
|
+
checkpointNumber,
|
|
354
|
+
archive: decodedArgs.archive,
|
|
355
|
+
header: decodedArgs.header,
|
|
356
|
+
l1BlockHash: blockHash,
|
|
357
|
+
attestations,
|
|
358
|
+
packedAttestations,
|
|
359
|
+
targetCommitteeSize: this.targetCommitteeSize
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
checkpointNumber,
|
|
363
|
+
archiveRoot,
|
|
364
|
+
header,
|
|
365
|
+
attestations,
|
|
366
|
+
blockHash,
|
|
367
|
+
feeAssetPriceModifier
|
|
368
|
+
};
|
|
369
|
+
} catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** Computes the keccak256 hash of ABI-encoded CommitteeAttestations. */ computeAttestationsHash(packedAttestations) {
|
|
374
|
+
return keccak256(encodeAbiParameters([
|
|
375
|
+
this.getCommitteeAttestationsStructDef()
|
|
376
|
+
], [
|
|
377
|
+
packedAttestations
|
|
378
|
+
]));
|
|
379
|
+
}
|
|
380
|
+
/** Computes the keccak256 payload digest from the checkpoint header, archive root, and fee asset price modifier. */ computePayloadDigest(header, archiveRoot, feeAssetPriceModifier) {
|
|
381
|
+
return computeCheckpointPayloadDigest({
|
|
382
|
+
header,
|
|
383
|
+
archiveRoot,
|
|
384
|
+
feeAssetPriceModifier,
|
|
385
|
+
signatureContext: this.getSignatureContext()
|
|
386
|
+
}).toString();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
303
389
|
* Extracts the CommitteeAttestations struct definition from RollupAbi.
|
|
304
390
|
* Finds the _attestations parameter by name in the propose function.
|
|
305
|
-
* Lazy-loaded to avoid issues during module initialization.
|
|
306
391
|
*/ getCommitteeAttestationsStructDef() {
|
|
307
392
|
const proposeFunction = RollupAbi.find((item)=>item.type === 'function' && item.name === 'propose');
|
|
308
393
|
if (!proposeFunction) {
|
|
@@ -323,162 +408,5 @@ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
|
323
408
|
components: tupleParam.components || []
|
|
324
409
|
};
|
|
325
410
|
}
|
|
326
|
-
/**
|
|
327
|
-
* Decodes propose calldata and builds the checkpoint header structure.
|
|
328
|
-
* @param proposeCalldata - The propose function calldata
|
|
329
|
-
* @param blockHash - The L1 block hash containing this transaction
|
|
330
|
-
* @param checkpointNumber - The checkpoint number
|
|
331
|
-
* @param expectedHashes - Optional expected hashes from the CheckpointProposed event for validation
|
|
332
|
-
* @returns The decoded checkpoint header and metadata
|
|
333
|
-
*/ decodeAndBuildCheckpoint(proposeCalldata, blockHash, checkpointNumber, expectedHashes) {
|
|
334
|
-
const { functionName: rollupFunctionName, args: rollupArgs } = decodeFunctionData({
|
|
335
|
-
abi: RollupAbi,
|
|
336
|
-
data: proposeCalldata
|
|
337
|
-
});
|
|
338
|
-
if (rollupFunctionName !== 'propose') {
|
|
339
|
-
throw new Error(`Unexpected rollup method called ${rollupFunctionName}`);
|
|
340
|
-
}
|
|
341
|
-
const [decodedArgs, packedAttestations, _signers, _attestationsAndSignersSignature, _blobInput] = rollupArgs;
|
|
342
|
-
const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
|
|
343
|
-
const header = CheckpointHeader.fromViem(decodedArgs.header);
|
|
344
|
-
const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
|
|
345
|
-
// Validate attestationsHash if provided (skip for backwards compatibility with older events)
|
|
346
|
-
if (expectedHashes.attestationsHash) {
|
|
347
|
-
// Compute attestationsHash: keccak256(abi.encode(CommitteeAttestations))
|
|
348
|
-
const computedAttestationsHash = keccak256(encodeAbiParameters([
|
|
349
|
-
this.getCommitteeAttestationsStructDef()
|
|
350
|
-
], [
|
|
351
|
-
packedAttestations
|
|
352
|
-
]));
|
|
353
|
-
// Compare as buffers to avoid case-sensitivity and string comparison issues
|
|
354
|
-
const computedBuffer = Buffer.from(hexToBytes(computedAttestationsHash));
|
|
355
|
-
const expectedBuffer = Buffer.from(hexToBytes(expectedHashes.attestationsHash));
|
|
356
|
-
if (!computedBuffer.equals(expectedBuffer)) {
|
|
357
|
-
throw new Error(`Attestations hash mismatch for checkpoint ${checkpointNumber}: ` + `computed=${computedAttestationsHash}, expected=${expectedHashes.attestationsHash}`);
|
|
358
|
-
}
|
|
359
|
-
this.logger.trace(`Validated attestationsHash for checkpoint ${checkpointNumber}`, {
|
|
360
|
-
computedAttestationsHash,
|
|
361
|
-
expectedAttestationsHash: expectedHashes.attestationsHash
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
// Validate payloadDigest if provided (skip for backwards compatibility with older events)
|
|
365
|
-
if (expectedHashes.payloadDigest) {
|
|
366
|
-
// Use ConsensusPayload to compute the digest - this ensures we match the exact logic
|
|
367
|
-
// used by the network for signing and verification
|
|
368
|
-
const feeAssetPriceModifier = decodedArgs.oracleInput.feeAssetPriceModifier;
|
|
369
|
-
const consensusPayload = new ConsensusPayload(header, archiveRoot, feeAssetPriceModifier);
|
|
370
|
-
const payloadToSign = consensusPayload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation);
|
|
371
|
-
const computedPayloadDigest = keccak256(payloadToSign);
|
|
372
|
-
// Compare as buffers to avoid case-sensitivity and string comparison issues
|
|
373
|
-
const computedBuffer = Buffer.from(hexToBytes(computedPayloadDigest));
|
|
374
|
-
const expectedBuffer = Buffer.from(hexToBytes(expectedHashes.payloadDigest));
|
|
375
|
-
if (!computedBuffer.equals(expectedBuffer)) {
|
|
376
|
-
throw new Error(`Payload digest mismatch for checkpoint ${checkpointNumber}: ` + `computed=${computedPayloadDigest}, expected=${expectedHashes.payloadDigest}`);
|
|
377
|
-
}
|
|
378
|
-
this.logger.trace(`Validated payloadDigest for checkpoint ${checkpointNumber}`, {
|
|
379
|
-
computedPayloadDigest,
|
|
380
|
-
expectedPayloadDigest: expectedHashes.payloadDigest
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
this.logger.trace(`Decoded propose calldata`, {
|
|
384
|
-
checkpointNumber,
|
|
385
|
-
archive: decodedArgs.archive,
|
|
386
|
-
header: decodedArgs.header,
|
|
387
|
-
l1BlockHash: blockHash,
|
|
388
|
-
attestations,
|
|
389
|
-
packedAttestations,
|
|
390
|
-
targetCommitteeSize: this.targetCommitteeSize
|
|
391
|
-
});
|
|
392
|
-
return {
|
|
393
|
-
checkpointNumber,
|
|
394
|
-
archiveRoot,
|
|
395
|
-
header,
|
|
396
|
-
attestations,
|
|
397
|
-
blockHash,
|
|
398
|
-
feeAssetPriceModifier: decodedArgs.oracleInput.feeAssetPriceModifier
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Pre-computed function selectors for all valid contract calls.
|
|
404
|
-
* These are computed once at module load time from the ABIs.
|
|
405
|
-
* Based on analysis of sequencer-client/src/publisher/sequencer-publisher.ts
|
|
406
|
-
*/ // Rollup contract function selectors (always valid)
|
|
407
|
-
const PROPOSE_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'propose'));
|
|
408
|
-
const INVALIDATE_BAD_ATTESTATION_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'invalidateBadAttestation'));
|
|
409
|
-
const INVALIDATE_INSUFFICIENT_ATTESTATIONS_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'invalidateInsufficientAttestations'));
|
|
410
|
-
// Governance proposer function selectors
|
|
411
|
-
const GOVERNANCE_SIGNAL_WITH_SIG_SELECTOR = toFunctionSelector(GovernanceProposerAbi.find((x)=>x.type === 'function' && x.name === 'signalWithSig'));
|
|
412
|
-
// Slash factory function selectors
|
|
413
|
-
const CREATE_SLASH_PAYLOAD_SELECTOR = toFunctionSelector(SlashFactoryAbi.find((x)=>x.type === 'function' && x.name === 'createSlashPayload'));
|
|
414
|
-
// Empire slashing proposer function selectors
|
|
415
|
-
const EMPIRE_SIGNAL_WITH_SIG_SELECTOR = toFunctionSelector(EmpireSlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'signalWithSig'));
|
|
416
|
-
const EMPIRE_SUBMIT_ROUND_WINNER_SELECTOR = toFunctionSelector(EmpireSlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'submitRoundWinner'));
|
|
417
|
-
// Tally slashing proposer function selectors
|
|
418
|
-
const TALLY_VOTE_SELECTOR = toFunctionSelector(TallySlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'vote'));
|
|
419
|
-
const TALLY_EXECUTE_ROUND_SELECTOR = toFunctionSelector(TallySlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'executeRound'));
|
|
420
|
-
/**
|
|
421
|
-
* All valid contract calls that the sequencer publisher can make.
|
|
422
|
-
* Builds the list of valid (address, selector) pairs for validation.
|
|
423
|
-
*
|
|
424
|
-
* Alternatively, if we are absolutely sure that no code path from any of these
|
|
425
|
-
* contracts can eventually land on another call to `propose`, we can remove the
|
|
426
|
-
* function selectors.
|
|
427
|
-
*/ function computeValidContractCalls(addresses) {
|
|
428
|
-
const { rollupAddress, governanceProposerAddress, slashFactoryAddress, slashingProposerAddress } = addresses;
|
|
429
|
-
const calls = [];
|
|
430
|
-
// Rollup contract calls (always present)
|
|
431
|
-
calls.push({
|
|
432
|
-
address: rollupAddress.toString().toLowerCase(),
|
|
433
|
-
functionSelector: PROPOSE_SELECTOR,
|
|
434
|
-
functionName: 'propose'
|
|
435
|
-
}, {
|
|
436
|
-
address: rollupAddress.toString().toLowerCase(),
|
|
437
|
-
functionSelector: INVALIDATE_BAD_ATTESTATION_SELECTOR,
|
|
438
|
-
functionName: 'invalidateBadAttestation'
|
|
439
|
-
}, {
|
|
440
|
-
address: rollupAddress.toString().toLowerCase(),
|
|
441
|
-
functionSelector: INVALIDATE_INSUFFICIENT_ATTESTATIONS_SELECTOR,
|
|
442
|
-
functionName: 'invalidateInsufficientAttestations'
|
|
443
|
-
});
|
|
444
|
-
// Governance proposer calls (optional)
|
|
445
|
-
if (governanceProposerAddress && !governanceProposerAddress.isZero()) {
|
|
446
|
-
calls.push({
|
|
447
|
-
address: governanceProposerAddress.toString().toLowerCase(),
|
|
448
|
-
functionSelector: GOVERNANCE_SIGNAL_WITH_SIG_SELECTOR,
|
|
449
|
-
functionName: 'signalWithSig'
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
// Slash factory calls (optional)
|
|
453
|
-
if (slashFactoryAddress && !slashFactoryAddress.isZero()) {
|
|
454
|
-
calls.push({
|
|
455
|
-
address: slashFactoryAddress.toString().toLowerCase(),
|
|
456
|
-
functionSelector: CREATE_SLASH_PAYLOAD_SELECTOR,
|
|
457
|
-
functionName: 'createSlashPayload'
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
// Slashing proposer calls (optional, can be either Empire or Tally)
|
|
461
|
-
if (slashingProposerAddress && !slashingProposerAddress.isZero()) {
|
|
462
|
-
// Empire calls
|
|
463
|
-
calls.push({
|
|
464
|
-
address: slashingProposerAddress.toString().toLowerCase(),
|
|
465
|
-
functionSelector: EMPIRE_SIGNAL_WITH_SIG_SELECTOR,
|
|
466
|
-
functionName: 'signalWithSig (empire)'
|
|
467
|
-
}, {
|
|
468
|
-
address: slashingProposerAddress.toString().toLowerCase(),
|
|
469
|
-
functionSelector: EMPIRE_SUBMIT_ROUND_WINNER_SELECTOR,
|
|
470
|
-
functionName: 'submitRoundWinner'
|
|
471
|
-
});
|
|
472
|
-
// Tally calls
|
|
473
|
-
calls.push({
|
|
474
|
-
address: slashingProposerAddress.toString().toLowerCase(),
|
|
475
|
-
functionSelector: TALLY_VOTE_SELECTOR,
|
|
476
|
-
functionName: 'vote'
|
|
477
|
-
}, {
|
|
478
|
-
address: slashingProposerAddress.toString().toLowerCase(),
|
|
479
|
-
functionSelector: TALLY_EXECUTE_ROUND_SELECTOR,
|
|
480
|
-
functionName: 'executeRound'
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
return calls;
|
|
484
411
|
}
|
|
412
|
+
/** Function selector for the `propose` method of the rollup contract. */ const PROPOSE_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'propose'));
|