@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.
Files changed (236) hide show
  1. package/README.md +164 -22
  2. package/dest/archiver.d.ts +194 -0
  3. package/dest/archiver.d.ts.map +1 -0
  4. package/dest/archiver.js +949 -0
  5. package/dest/config.d.ts +34 -0
  6. package/dest/config.d.ts.map +1 -0
  7. package/dest/config.js +95 -0
  8. package/dest/errors.d.ts +87 -0
  9. package/dest/errors.d.ts.map +1 -0
  10. package/dest/errors.js +129 -0
  11. package/dest/factory.d.ts +16 -10
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +117 -20
  14. package/dest/index.d.ts +19 -4
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +17 -3
  17. package/dest/interfaces.d.ts +9 -0
  18. package/dest/interfaces.d.ts.map +1 -0
  19. package/dest/interfaces.js +3 -0
  20. package/dest/l1/bin/retrieve-calldata.d.ts +3 -0
  21. package/dest/l1/bin/retrieve-calldata.d.ts.map +1 -0
  22. package/dest/l1/bin/retrieve-calldata.js +152 -0
  23. package/dest/l1/calldata_retriever.d.ts +143 -0
  24. package/dest/l1/calldata_retriever.d.ts.map +1 -0
  25. package/dest/l1/calldata_retriever.js +413 -0
  26. package/dest/l1/data_retrieval.d.ts +102 -0
  27. package/dest/l1/data_retrieval.d.ts.map +1 -0
  28. package/dest/{archiver → l1}/data_retrieval.js +85 -165
  29. package/dest/l1/debug_tx.d.ts +19 -0
  30. package/dest/l1/debug_tx.d.ts.map +1 -0
  31. package/dest/l1/debug_tx.js +73 -0
  32. package/dest/l1/spire_proposer.d.ts +70 -0
  33. package/dest/l1/spire_proposer.d.ts.map +1 -0
  34. package/dest/l1/spire_proposer.js +149 -0
  35. package/dest/l1/trace_tx.d.ts +43 -0
  36. package/dest/l1/trace_tx.d.ts.map +1 -0
  37. package/dest/l1/trace_tx.js +91 -0
  38. package/dest/l1/types.d.ts +12 -0
  39. package/dest/l1/types.d.ts.map +1 -0
  40. package/dest/l1/types.js +3 -0
  41. package/dest/l1/validate_historical_logs.d.ts +23 -0
  42. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  43. package/dest/l1/validate_historical_logs.js +108 -0
  44. package/dest/l1/validate_trace.d.ts +32 -0
  45. package/dest/l1/validate_trace.d.ts.map +1 -0
  46. package/dest/l1/validate_trace.js +154 -0
  47. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  48. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  49. package/dest/modules/contract_data_source_adapter.js +32 -0
  50. package/dest/modules/data_source_base.d.ts +112 -0
  51. package/dest/modules/data_source_base.d.ts.map +1 -0
  52. package/dest/modules/data_source_base.js +322 -0
  53. package/dest/modules/data_store_updater.d.ts +105 -0
  54. package/dest/modules/data_store_updater.d.ts.map +1 -0
  55. package/dest/modules/data_store_updater.js +393 -0
  56. package/dest/modules/instrumentation.d.ts +63 -0
  57. package/dest/modules/instrumentation.d.ts.map +1 -0
  58. package/dest/modules/instrumentation.js +166 -0
  59. package/dest/modules/l1_synchronizer.d.ts +77 -0
  60. package/dest/modules/l1_synchronizer.d.ts.map +1 -0
  61. package/dest/modules/l1_synchronizer.js +1381 -0
  62. package/dest/modules/outbox_trees_resolver.d.ts +62 -0
  63. package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
  64. package/dest/modules/outbox_trees_resolver.js +162 -0
  65. package/dest/modules/validation.d.ts +40 -0
  66. package/dest/modules/validation.d.ts.map +1 -0
  67. package/dest/{archiver → modules}/validation.js +41 -17
  68. package/dest/store/block_store.d.ts +303 -0
  69. package/dest/store/block_store.d.ts.map +1 -0
  70. package/dest/store/block_store.js +1206 -0
  71. package/dest/store/contract_class_store.d.ts +31 -0
  72. package/dest/store/contract_class_store.d.ts.map +1 -0
  73. package/dest/store/contract_class_store.js +98 -0
  74. package/dest/store/contract_instance_store.d.ts +51 -0
  75. package/dest/store/contract_instance_store.d.ts.map +1 -0
  76. package/dest/{archiver/kv_archiver_store → store}/contract_instance_store.js +55 -3
  77. package/dest/store/data_stores.d.ts +68 -0
  78. package/dest/store/data_stores.d.ts.map +1 -0
  79. package/dest/store/data_stores.js +54 -0
  80. package/dest/store/function_names_cache.d.ts +17 -0
  81. package/dest/store/function_names_cache.d.ts.map +1 -0
  82. package/dest/store/function_names_cache.js +30 -0
  83. package/dest/store/l2_tips_cache.d.ts +25 -0
  84. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  85. package/dest/store/l2_tips_cache.js +26 -0
  86. package/dest/store/log_store.d.ts +59 -0
  87. package/dest/store/log_store.d.ts.map +1 -0
  88. package/dest/store/log_store.js +310 -0
  89. package/dest/store/log_store_codec.d.ts +78 -0
  90. package/dest/store/log_store_codec.d.ts.map +1 -0
  91. package/dest/store/log_store_codec.js +110 -0
  92. package/dest/store/message_store.d.ts +50 -0
  93. package/dest/store/message_store.d.ts.map +1 -0
  94. package/dest/{archiver/kv_archiver_store → store}/message_store.js +66 -23
  95. package/dest/{archiver/structs → structs}/data_retrieval.d.ts +1 -1
  96. package/dest/structs/data_retrieval.d.ts.map +1 -0
  97. package/dest/structs/inbox_message.d.ts +15 -0
  98. package/dest/structs/inbox_message.d.ts.map +1 -0
  99. package/dest/{archiver/structs → structs}/inbox_message.js +6 -6
  100. package/dest/structs/published.d.ts +2 -0
  101. package/dest/structs/published.d.ts.map +1 -0
  102. package/dest/test/fake_l1_state.d.ts +214 -0
  103. package/dest/test/fake_l1_state.d.ts.map +1 -0
  104. package/dest/test/fake_l1_state.js +517 -0
  105. package/dest/test/index.d.ts +2 -1
  106. package/dest/test/index.d.ts.map +1 -1
  107. package/dest/test/index.js +4 -1
  108. package/dest/test/mock_archiver.d.ts +5 -6
  109. package/dest/test/mock_archiver.d.ts.map +1 -1
  110. package/dest/test/mock_archiver.js +8 -12
  111. package/dest/test/mock_l1_to_l2_message_source.d.ts +6 -7
  112. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  113. package/dest/test/mock_l1_to_l2_message_source.js +19 -14
  114. package/dest/test/mock_l2_block_source.d.ts +68 -40
  115. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  116. package/dest/test/mock_l2_block_source.js +347 -145
  117. package/dest/test/mock_structs.d.ts +83 -4
  118. package/dest/test/mock_structs.d.ts.map +1 -1
  119. package/dest/test/mock_structs.js +157 -12
  120. package/dest/test/noop_l1_archiver.d.ts +34 -0
  121. package/dest/test/noop_l1_archiver.d.ts.map +1 -0
  122. package/dest/test/noop_l1_archiver.js +88 -0
  123. package/package.json +18 -18
  124. package/src/archiver.ts +785 -0
  125. package/src/config.ts +125 -0
  126. package/src/errors.ts +203 -0
  127. package/src/factory.ts +185 -22
  128. package/src/index.ts +27 -3
  129. package/src/interfaces.ts +9 -0
  130. package/src/l1/README.md +55 -0
  131. package/src/l1/bin/retrieve-calldata.ts +194 -0
  132. package/src/l1/calldata_retriever.ts +529 -0
  133. package/src/{archiver → l1}/data_retrieval.ts +157 -243
  134. package/src/l1/debug_tx.ts +99 -0
  135. package/src/l1/spire_proposer.ts +152 -0
  136. package/src/l1/trace_tx.ts +128 -0
  137. package/src/l1/types.ts +13 -0
  138. package/src/l1/validate_historical_logs.ts +140 -0
  139. package/src/l1/validate_trace.ts +229 -0
  140. package/src/modules/contract_data_source_adapter.ts +46 -0
  141. package/src/modules/data_source_base.ts +466 -0
  142. package/src/modules/data_store_updater.ts +519 -0
  143. package/src/modules/instrumentation.ts +217 -0
  144. package/src/modules/l1_synchronizer.ts +1320 -0
  145. package/src/modules/outbox_trees_resolver.ts +199 -0
  146. package/src/modules/validation.ts +186 -0
  147. package/src/store/block_store.ts +1551 -0
  148. package/src/store/contract_class_store.ts +126 -0
  149. package/src/{archiver/kv_archiver_store → store}/contract_instance_store.ts +70 -7
  150. package/src/store/data_stores.ts +104 -0
  151. package/src/store/function_names_cache.ts +37 -0
  152. package/src/store/l2_tips_cache.ts +35 -0
  153. package/src/store/log_store.ts +380 -0
  154. package/src/store/log_store_codec.ts +143 -0
  155. package/src/{archiver/kv_archiver_store → store}/message_store.ts +81 -28
  156. package/src/{archiver/structs → structs}/inbox_message.ts +8 -9
  157. package/src/{archiver/structs → structs}/published.ts +0 -1
  158. package/src/test/fake_l1_state.ts +770 -0
  159. package/src/test/fixtures/debug_traceTransaction-multicall3.json +88 -0
  160. package/src/test/fixtures/debug_traceTransaction-multiplePropose.json +153 -0
  161. package/src/test/fixtures/debug_traceTransaction-proxied.json +122 -0
  162. package/src/test/fixtures/trace_transaction-multicall3.json +65 -0
  163. package/src/test/fixtures/trace_transaction-multiplePropose.json +319 -0
  164. package/src/test/fixtures/trace_transaction-proxied.json +128 -0
  165. package/src/test/fixtures/trace_transaction-randomRevert.json +216 -0
  166. package/src/test/index.ts +4 -0
  167. package/src/test/mock_archiver.ts +10 -14
  168. package/src/test/mock_l1_to_l2_message_source.ts +16 -15
  169. package/src/test/mock_l2_block_source.ts +416 -161
  170. package/src/test/mock_structs.ts +292 -14
  171. package/src/test/noop_l1_archiver.ts +158 -0
  172. package/dest/archiver/archiver.d.ts +0 -290
  173. package/dest/archiver/archiver.d.ts.map +0 -1
  174. package/dest/archiver/archiver.js +0 -1434
  175. package/dest/archiver/archiver_store.d.ts +0 -256
  176. package/dest/archiver/archiver_store.d.ts.map +0 -1
  177. package/dest/archiver/archiver_store.js +0 -4
  178. package/dest/archiver/archiver_store_test_suite.d.ts +0 -8
  179. package/dest/archiver/archiver_store_test_suite.d.ts.map +0 -1
  180. package/dest/archiver/archiver_store_test_suite.js +0 -1289
  181. package/dest/archiver/config.d.ts +0 -21
  182. package/dest/archiver/config.d.ts.map +0 -1
  183. package/dest/archiver/config.js +0 -55
  184. package/dest/archiver/data_retrieval.d.ts +0 -80
  185. package/dest/archiver/data_retrieval.d.ts.map +0 -1
  186. package/dest/archiver/errors.d.ts +0 -12
  187. package/dest/archiver/errors.d.ts.map +0 -1
  188. package/dest/archiver/errors.js +0 -17
  189. package/dest/archiver/index.d.ts +0 -7
  190. package/dest/archiver/index.d.ts.map +0 -1
  191. package/dest/archiver/index.js +0 -4
  192. package/dest/archiver/instrumentation.d.ts +0 -35
  193. package/dest/archiver/instrumentation.d.ts.map +0 -1
  194. package/dest/archiver/instrumentation.js +0 -140
  195. package/dest/archiver/kv_archiver_store/block_store.d.ts +0 -125
  196. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +0 -1
  197. package/dest/archiver/kv_archiver_store/block_store.js +0 -371
  198. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +0 -18
  199. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +0 -1
  200. package/dest/archiver/kv_archiver_store/contract_class_store.js +0 -120
  201. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +0 -24
  202. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +0 -1
  203. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +0 -169
  204. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +0 -1
  205. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +0 -296
  206. package/dest/archiver/kv_archiver_store/log_store.d.ts +0 -49
  207. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +0 -1
  208. package/dest/archiver/kv_archiver_store/log_store.js +0 -337
  209. package/dest/archiver/kv_archiver_store/message_store.d.ts +0 -39
  210. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +0 -1
  211. package/dest/archiver/structs/data_retrieval.d.ts.map +0 -1
  212. package/dest/archiver/structs/inbox_message.d.ts +0 -15
  213. package/dest/archiver/structs/inbox_message.d.ts.map +0 -1
  214. package/dest/archiver/structs/published.d.ts +0 -3
  215. package/dest/archiver/structs/published.d.ts.map +0 -1
  216. package/dest/archiver/validation.d.ts +0 -17
  217. package/dest/archiver/validation.d.ts.map +0 -1
  218. package/dest/rpc/index.d.ts +0 -9
  219. package/dest/rpc/index.d.ts.map +0 -1
  220. package/dest/rpc/index.js +0 -15
  221. package/src/archiver/archiver.ts +0 -1880
  222. package/src/archiver/archiver_store.ts +0 -310
  223. package/src/archiver/archiver_store_test_suite.ts +0 -1295
  224. package/src/archiver/config.ts +0 -79
  225. package/src/archiver/errors.ts +0 -26
  226. package/src/archiver/index.ts +0 -6
  227. package/src/archiver/instrumentation.ts +0 -187
  228. package/src/archiver/kv_archiver_store/block_store.ts +0 -482
  229. package/src/archiver/kv_archiver_store/contract_class_store.ts +0 -176
  230. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +0 -423
  231. package/src/archiver/kv_archiver_store/log_store.ts +0 -407
  232. package/src/archiver/validation.ts +0 -124
  233. package/src/rpc/index.ts +0 -16
  234. /package/dest/{archiver/structs → structs}/data_retrieval.js +0 -0
  235. /package/dest/{archiver/structs → structs}/published.js +0 -0
  236. /package/src/{archiver/structs → structs}/data_retrieval.ts +0 -0
@@ -0,0 +1,529 @@
1
+ import { MULTI_CALL_3_ADDRESS, type ViemCommitteeAttestations, type ViemHeader } from '@aztec/ethereum/contracts';
2
+ import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
3
+ import { CheckpointNumber } from '@aztec/foundation/branded-types';
4
+ import { LruSet } from '@aztec/foundation/collection';
5
+ import { Fr } from '@aztec/foundation/curves/bn254';
6
+ import { EthAddress } from '@aztec/foundation/eth-address';
7
+ import type { Logger } from '@aztec/foundation/log';
8
+ import { RollupAbi } from '@aztec/l1-artifacts';
9
+ import { CommitteeAttestation } from '@aztec/stdlib/block';
10
+ import { computeCheckpointPayloadDigest } from '@aztec/stdlib/checkpoint';
11
+ import { CheckpointHeader } from '@aztec/stdlib/rollup';
12
+
13
+ import {
14
+ type AbiParameter,
15
+ type Hex,
16
+ type Transaction,
17
+ decodeFunctionData,
18
+ encodeAbiParameters,
19
+ hexToBytes,
20
+ keccak256,
21
+ multicall3Abi,
22
+ toFunctionSelector,
23
+ } from 'viem';
24
+
25
+ import type { ArchiverInstrumentation } from '../modules/instrumentation.js';
26
+ import { getSuccessfulCallsFromDebug } from './debug_tx.js';
27
+ import { getCallsFromSpireProposer } from './spire_proposer.js';
28
+ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
29
+ import type { CallInfo } from './types.js';
30
+
31
+ /** Decoded checkpoint data from a propose calldata. */
32
+ type CheckpointData = {
33
+ checkpointNumber: CheckpointNumber;
34
+ archiveRoot: Fr;
35
+ header: CheckpointHeader;
36
+ attestations: CommitteeAttestation[];
37
+ /**
38
+ * The exact packed `CommitteeAttestations` tuple as it appears in the propose calldata, preserved
39
+ * verbatim (never re-derived from {@link attestations}) so invalidation evidence stays byte-faithful to
40
+ * the on-chain `attestationsHash`.
41
+ */
42
+ verbatimAttestations: ViemCommitteeAttestations;
43
+ blockHash: string;
44
+ feeAssetPriceModifier: bigint;
45
+ };
46
+
47
+ /**
48
+ * Extracts calldata to the `propose` method of the rollup contract from an L1 transaction
49
+ * in order to reconstruct an L2 block header. Uses hash matching against expected hashes
50
+ * from the CheckpointProposed event to verify the correct propose calldata.
51
+ */
52
+ export class CalldataRetriever {
53
+ /** Tx hashes we've already logged for trace+debug failure (log once per tx per process). */
54
+ private static readonly traceFailureWarnedTxHashes = new LruSet<string>(1000);
55
+
56
+ /** Clears the trace-failure warned set. For testing only. */
57
+ static resetTraceFailureWarnedForTesting(): void {
58
+ CalldataRetriever.traceFailureWarnedTxHashes.clear();
59
+ }
60
+
61
+ constructor(
62
+ private readonly publicClient: ViemPublicClient,
63
+ private readonly debugClient: ViemPublicDebugClient,
64
+ private readonly targetCommitteeSize: number,
65
+ private readonly instrumentation: ArchiverInstrumentation | undefined,
66
+ private readonly logger: Logger,
67
+ private readonly rollupAddress: EthAddress,
68
+ ) {}
69
+
70
+ private getSignatureContext() {
71
+ return {
72
+ chainId: this.publicClient.chain.id,
73
+ rollupAddress: this.rollupAddress,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Gets checkpoint header and metadata from the calldata of an L1 transaction.
79
+ * Tries multicall3 decoding, falls back to trace-based extraction.
80
+ * @param txHash - Hash of the tx that published it.
81
+ * @param blobHashes - Blob hashes for the checkpoint.
82
+ * @param checkpointNumber - Checkpoint number.
83
+ * @param expectedHashes - Expected hashes from the CheckpointProposed event for validation
84
+ * @returns Checkpoint header and metadata from the calldata, deserialized
85
+ */
86
+ async getCheckpointFromRollupTx(
87
+ txHash: `0x${string}`,
88
+ _blobHashes: Buffer[],
89
+ checkpointNumber: CheckpointNumber,
90
+ expectedHashes: {
91
+ attestationsHash: Hex;
92
+ payloadDigest: Hex;
93
+ },
94
+ ): Promise<CheckpointData> {
95
+ this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`);
96
+ const tx = await this.publicClient.getTransaction({ hash: txHash });
97
+ return this.getCheckpointFromTx(tx, checkpointNumber, expectedHashes);
98
+ }
99
+
100
+ /** Gets checkpoint data from a transaction by trying decode strategies then falling back to trace. */
101
+ protected async getCheckpointFromTx(
102
+ tx: Transaction,
103
+ checkpointNumber: CheckpointNumber,
104
+ expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
105
+ ): Promise<CheckpointData> {
106
+ // Try to decode as multicall3 with hash-verified matching
107
+ const multicall3Result = this.tryDecodeMulticall3(tx, expectedHashes, checkpointNumber, tx.blockHash!);
108
+ if (multicall3Result) {
109
+ this.logger.trace(`Decoded propose calldata from multicall3 for tx ${tx.hash}`);
110
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to!, false);
111
+ return multicall3Result;
112
+ }
113
+
114
+ // Try to decode as direct propose call
115
+ const directResult = this.tryDecodeDirectPropose(tx, expectedHashes, checkpointNumber, tx.blockHash!);
116
+ if (directResult) {
117
+ this.logger.trace(`Decoded propose calldata from direct call for tx ${tx.hash}`);
118
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to!, false);
119
+ return directResult;
120
+ }
121
+
122
+ // Try to decode as Spire Proposer multicall wrapper
123
+ const spireResult = await this.tryDecodeSpireProposer(tx, expectedHashes, checkpointNumber, tx.blockHash!);
124
+ if (spireResult) {
125
+ this.logger.trace(`Decoded propose calldata from Spire Proposer for tx ${tx.hash}`);
126
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to!, false);
127
+ return spireResult;
128
+ }
129
+
130
+ // Fall back to trace-based extraction
131
+ this.logger.warn(
132
+ `Failed to decode multicall3, direct propose, or Spire proposer for L1 tx ${tx.hash}, falling back to trace for checkpoint ${checkpointNumber}`,
133
+ );
134
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to ?? EthAddress.ZERO.toString(), true);
135
+ const tracedCalldata = await this.extractCalldataViaTrace(tx.hash);
136
+ const tracedResult = this.tryDecodeAndVerifyPropose(
137
+ tracedCalldata,
138
+ expectedHashes,
139
+ checkpointNumber,
140
+ tx.blockHash!,
141
+ );
142
+ if (!tracedResult) {
143
+ throw new Error(`Hash mismatch for traced propose calldata in tx ${tx.hash} for checkpoint ${checkpointNumber}`);
144
+ }
145
+ return tracedResult;
146
+ }
147
+
148
+ /**
149
+ * Attempts to decode a transaction as a Spire Proposer multicall wrapper.
150
+ * If successful, iterates all wrapped calls and validates each as either multicall3
151
+ * or direct propose, verifying against expected hashes.
152
+ * @param tx - The transaction to decode
153
+ * @param expectedHashes - Expected hashes for hash-verified matching
154
+ * @param checkpointNumber - The checkpoint number
155
+ * @param blockHash - The L1 block hash
156
+ * @returns The checkpoint data if successfully decoded and validated, undefined otherwise
157
+ */
158
+ protected async tryDecodeSpireProposer(
159
+ tx: Transaction,
160
+ expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
161
+ checkpointNumber: CheckpointNumber,
162
+ blockHash: Hex,
163
+ ): Promise<CheckpointData | undefined> {
164
+ // Try to decode as Spire Proposer multicall (extracts all wrapped calls)
165
+ const spireWrappedCalls = await getCallsFromSpireProposer(tx, this.publicClient, this.logger);
166
+ if (!spireWrappedCalls) {
167
+ return undefined;
168
+ }
169
+
170
+ this.logger.trace(`Decoded Spire Proposer wrapping for tx ${tx.hash}, ${spireWrappedCalls.length} inner call(s)`);
171
+
172
+ // Try each wrapped call as either multicall3 or direct propose
173
+ for (const spireWrappedCall of spireWrappedCalls) {
174
+ const wrappedTx = { to: spireWrappedCall.to, input: spireWrappedCall.data, hash: tx.hash };
175
+
176
+ const multicall3Result = this.tryDecodeMulticall3(wrappedTx, expectedHashes, checkpointNumber, blockHash);
177
+ if (multicall3Result) {
178
+ this.logger.trace(`Decoded propose calldata from Spire Proposer to multicall3 for tx ${tx.hash}`);
179
+ return multicall3Result;
180
+ }
181
+
182
+ const directResult = this.tryDecodeDirectPropose(wrappedTx, expectedHashes, checkpointNumber, blockHash);
183
+ if (directResult) {
184
+ this.logger.trace(`Decoded propose calldata from Spire Proposer to direct propose for tx ${tx.hash}`);
185
+ return directResult;
186
+ }
187
+ }
188
+
189
+ this.logger.warn(
190
+ `Spire Proposer wrapped calls could not be decoded as multicall3 or direct propose for tx ${tx.hash}`,
191
+ );
192
+ return undefined;
193
+ }
194
+
195
+ /**
196
+ * Attempts to decode transaction input as multicall3 and extract propose calldata.
197
+ * Finds all calls matching the rollup address and propose selector, then decodes
198
+ * and verifies each candidate against expected hashes from the CheckpointProposed event.
199
+ * @param tx - The transaction-like object with to, input, and hash
200
+ * @param expectedHashes - Expected hashes from CheckpointProposed event
201
+ * @param checkpointNumber - The checkpoint number
202
+ * @param blockHash - The L1 block hash
203
+ * @returns The checkpoint data if successfully validated, undefined otherwise
204
+ */
205
+ protected tryDecodeMulticall3(
206
+ tx: { to: Hex | null | undefined; input: Hex; hash: Hex },
207
+ expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
208
+ checkpointNumber: CheckpointNumber,
209
+ blockHash: Hex,
210
+ ): CheckpointData | undefined {
211
+ const txHash = tx.hash;
212
+
213
+ try {
214
+ // Check if transaction is to Multicall3 address
215
+ if (!tx.to || !EthAddress.areEqual(tx.to, MULTI_CALL_3_ADDRESS)) {
216
+ this.logger.debug(`Transaction is not to Multicall3 address (to: ${tx.to})`, { txHash, to: tx.to });
217
+ return undefined;
218
+ }
219
+
220
+ // Try to decode as multicall3 aggregate3 call
221
+ const { functionName: multicall3Fn, args: multicall3Args } = decodeFunctionData({
222
+ abi: multicall3Abi,
223
+ data: tx.input,
224
+ });
225
+
226
+ // If not aggregate3, return undefined (not a multicall3 transaction)
227
+ if (multicall3Fn !== 'aggregate3') {
228
+ this.logger.warn(`Transaction is not multicall3 aggregate3 (got ${multicall3Fn})`, { txHash });
229
+ return undefined;
230
+ }
231
+
232
+ if (multicall3Args.length !== 1) {
233
+ this.logger.warn(`Unexpected number of arguments for multicall3 (got ${multicall3Args.length})`, { txHash });
234
+ return undefined;
235
+ }
236
+
237
+ const [calls] = multicall3Args;
238
+
239
+ // Find all calls matching rollup address + propose selector
240
+ const rollupAddressLower = this.rollupAddress.toString().toLowerCase();
241
+ const proposeSelectorLower = PROPOSE_SELECTOR.toLowerCase();
242
+ const candidates: Hex[] = [];
243
+
244
+ for (const call of calls) {
245
+ const addr = call.target.toLowerCase();
246
+ const callData = call.callData;
247
+
248
+ if (callData.length < 10) {
249
+ continue;
250
+ }
251
+
252
+ const selector = callData.slice(0, 10).toLowerCase();
253
+ if (addr === rollupAddressLower && selector === proposeSelectorLower) {
254
+ candidates.push(callData);
255
+ }
256
+ }
257
+
258
+ if (candidates.length === 0) {
259
+ this.logger.debug(`No propose candidates found in multicall3`, { txHash });
260
+ return undefined;
261
+ }
262
+
263
+ // Decode, verify, and build for each candidate
264
+ const verified: CheckpointData[] = [];
265
+ for (const candidate of candidates) {
266
+ const result = this.tryDecodeAndVerifyPropose(candidate, expectedHashes, checkpointNumber, blockHash);
267
+ if (result) {
268
+ verified.push(result);
269
+ }
270
+ }
271
+
272
+ if (verified.length === 1) {
273
+ this.logger.trace(`Verified single propose candidate via hash matching`, { txHash });
274
+ return verified[0];
275
+ }
276
+
277
+ if (verified.length > 1) {
278
+ this.logger.warn(
279
+ `Multiple propose candidates verified (${verified.length}), returning first (identical data)`,
280
+ { txHash },
281
+ );
282
+ return verified[0];
283
+ }
284
+
285
+ this.logger.debug(`No candidates verified against expected hashes`, { txHash });
286
+ return undefined;
287
+ } catch (err) {
288
+ // Any decoding error triggers fallback to trace
289
+ this.logger.warn(`Failed to decode multicall3: ${err}`, { txHash });
290
+ return undefined;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Attempts to decode transaction as a direct propose call to the rollup contract.
296
+ * Decodes, verifies hashes, and builds checkpoint data in a single pass.
297
+ * @param tx - The transaction-like object with to, input, and hash
298
+ * @param expectedHashes - Expected hashes from CheckpointProposed event
299
+ * @param checkpointNumber - The checkpoint number
300
+ * @param blockHash - The L1 block hash
301
+ * @returns The checkpoint data if successfully validated, undefined otherwise
302
+ */
303
+ protected tryDecodeDirectPropose(
304
+ tx: { to: Hex | null | undefined; input: Hex; hash: Hex },
305
+ expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
306
+ checkpointNumber: CheckpointNumber,
307
+ blockHash: Hex,
308
+ ): CheckpointData | undefined {
309
+ const txHash = tx.hash;
310
+ try {
311
+ // Check if transaction is to the rollup address
312
+ if (!tx.to || !EthAddress.areEqual(tx.to, this.rollupAddress)) {
313
+ this.logger.debug(`Transaction is not to rollup address (to: ${tx.to})`, { txHash });
314
+ return undefined;
315
+ }
316
+
317
+ // Validate it's a propose call before full decode+verify
318
+ const { functionName } = decodeFunctionData({ abi: RollupAbi, data: tx.input });
319
+ if (functionName !== 'propose') {
320
+ this.logger.warn(`Transaction to rollup is not propose (got ${functionName})`, { txHash });
321
+ return undefined;
322
+ }
323
+
324
+ // Decode, verify hashes, and build checkpoint data
325
+ this.logger.trace(`Validated direct propose call to rollup`, { txHash });
326
+ return this.tryDecodeAndVerifyPropose(tx.input, expectedHashes, checkpointNumber, blockHash);
327
+ } catch (err) {
328
+ // Any decoding error means it's not a valid propose call
329
+ this.logger.warn(`Failed to decode as direct propose: ${err}`, { txHash });
330
+ return undefined;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Uses debug/trace RPC to extract the actual calldata from the successful propose call.
336
+ * This is the definitive fallback that works for any transaction pattern.
337
+ * Tries trace_transaction first, then falls back to debug_traceTransaction.
338
+ * @param txHash - The transaction hash to trace
339
+ * @returns The propose calldata from the successful call
340
+ */
341
+ protected async extractCalldataViaTrace(txHash: Hex): Promise<Hex> {
342
+ const rollupAddress = this.rollupAddress;
343
+ const selector = PROPOSE_SELECTOR;
344
+
345
+ let calls: CallInfo[];
346
+ try {
347
+ // Try trace_transaction first (using Parity/OpenEthereum/Erigon RPC)
348
+ this.logger.debug(`Attempting to trace transaction ${txHash} using trace_transaction`);
349
+ calls = await getSuccessfulCallsFromTrace(this.debugClient, txHash, rollupAddress, selector, this.logger);
350
+ this.logger.debug(`Successfully traced using trace_transaction, found ${calls.length} calls`);
351
+ } catch (err) {
352
+ const traceError = err instanceof Error ? err : new Error(String(err));
353
+ this.logger.verbose(`Failed trace_transaction for ${txHash}: ${traceError.message}`);
354
+ this.logger.debug(`Trace failure details for ${txHash}`, { traceError });
355
+
356
+ try {
357
+ // Fall back to debug_traceTransaction (Geth RPC)
358
+ this.logger.debug(`Attempting to trace transaction ${txHash} using debug_traceTransaction`);
359
+ calls = await getSuccessfulCallsFromDebug(this.debugClient, txHash, rollupAddress, selector, this.logger);
360
+ this.logger.debug(`Successfully traced using debug_traceTransaction, found ${calls.length} calls`);
361
+ } catch (debugErr) {
362
+ const debugError = debugErr instanceof Error ? debugErr : new Error(String(debugErr));
363
+ // Log once per tx so we don't spam on every sync cycle when sync point doesn't advance
364
+ if (!CalldataRetriever.traceFailureWarnedTxHashes.has(txHash)) {
365
+ CalldataRetriever.traceFailureWarnedTxHashes.add(txHash);
366
+ this.logger.warn(
367
+ `Cannot decode L1 tx ${txHash}: trace and debug RPC failed or unavailable. ` +
368
+ `trace_transaction: ${traceError.message}; debug_traceTransaction: ${debugError.message}`,
369
+ );
370
+ }
371
+ // Full error objects can be very long; keep at debug only
372
+ this.logger.debug(`Trace/debug failure details for tx ${txHash}`, {
373
+ traceError,
374
+ debugError,
375
+ txHash,
376
+ });
377
+ throw new Error(`Failed to trace transaction ${txHash} to extract propose calldata`);
378
+ }
379
+ }
380
+
381
+ // Validate exactly ONE successful propose call
382
+ if (calls.length === 0) {
383
+ throw new Error(`No successful propose calls found in transaction ${txHash}`);
384
+ }
385
+
386
+ if (calls.length > 1) {
387
+ throw new Error(`Multiple successful propose calls found in transaction ${txHash} (${calls.length})`);
388
+ }
389
+
390
+ // Return the calldata from the single successful propose call
391
+ return calls[0].input;
392
+ }
393
+
394
+ /**
395
+ * Decodes propose calldata, verifies against expected hashes, and builds checkpoint data.
396
+ * Returns undefined on decode errors or hash mismatches (soft failure for try-based callers).
397
+ * @param proposeCalldata - The propose function calldata
398
+ * @param expectedHashes - Expected hashes from the CheckpointProposed event
399
+ * @param checkpointNumber - The checkpoint number
400
+ * @param blockHash - The L1 block hash
401
+ * @returns The decoded checkpoint data, or undefined on failure
402
+ */
403
+ protected tryDecodeAndVerifyPropose(
404
+ proposeCalldata: Hex,
405
+ expectedHashes: { attestationsHash: Hex; payloadDigest: Hex },
406
+ checkpointNumber: CheckpointNumber,
407
+ blockHash: Hex,
408
+ ): CheckpointData | undefined {
409
+ try {
410
+ const { functionName, args } = decodeFunctionData({ abi: RollupAbi, data: proposeCalldata });
411
+ if (functionName !== 'propose') {
412
+ return undefined;
413
+ }
414
+
415
+ const [decodedArgs, verbatimAttestations] = args! as readonly [
416
+ { archive: Hex; oracleInput: { feeAssetPriceModifier: bigint }; header: ViemHeader },
417
+ ViemCommitteeAttestations,
418
+ ...unknown[],
419
+ ];
420
+
421
+ // Verify attestationsHash
422
+ const computedAttestationsHash = this.computeAttestationsHash(verbatimAttestations);
423
+ if (
424
+ !Buffer.from(hexToBytes(computedAttestationsHash)).equals(
425
+ Buffer.from(hexToBytes(expectedHashes.attestationsHash)),
426
+ )
427
+ ) {
428
+ this.logger.warn(`Attestations hash mismatch during verification`, {
429
+ computed: computedAttestationsHash,
430
+ expected: expectedHashes.attestationsHash,
431
+ });
432
+ return undefined;
433
+ }
434
+
435
+ // Verify payloadDigest
436
+ const header = CheckpointHeader.fromViem(decodedArgs.header);
437
+ const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
438
+ const feeAssetPriceModifier = decodedArgs.oracleInput.feeAssetPriceModifier;
439
+ const computedPayloadDigest = this.computePayloadDigest(header, archiveRoot, feeAssetPriceModifier);
440
+ if (
441
+ !Buffer.from(hexToBytes(computedPayloadDigest)).equals(Buffer.from(hexToBytes(expectedHashes.payloadDigest)))
442
+ ) {
443
+ this.logger.warn(`Payload digest mismatch during verification`, {
444
+ computed: computedPayloadDigest,
445
+ expected: expectedHashes.payloadDigest,
446
+ });
447
+ return undefined;
448
+ }
449
+
450
+ const attestations = CommitteeAttestation.fromPacked(verbatimAttestations, this.targetCommitteeSize);
451
+
452
+ this.logger.trace(`Validated and decoded propose calldata for checkpoint ${checkpointNumber}`, {
453
+ checkpointNumber,
454
+ archive: decodedArgs.archive,
455
+ header: decodedArgs.header,
456
+ l1BlockHash: blockHash,
457
+ attestations,
458
+ verbatimAttestations,
459
+ targetCommitteeSize: this.targetCommitteeSize,
460
+ });
461
+
462
+ return {
463
+ checkpointNumber,
464
+ archiveRoot,
465
+ header,
466
+ attestations,
467
+ verbatimAttestations,
468
+ blockHash,
469
+ feeAssetPriceModifier,
470
+ };
471
+ } catch {
472
+ return undefined;
473
+ }
474
+ }
475
+
476
+ /** Computes the keccak256 hash of ABI-encoded CommitteeAttestations. */
477
+ private computeAttestationsHash(verbatimAttestations: ViemCommitteeAttestations): Hex {
478
+ return keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations]));
479
+ }
480
+
481
+ /** Computes the keccak256 payload digest from the checkpoint header, archive root, and fee asset price modifier. */
482
+ private computePayloadDigest(header: CheckpointHeader, archiveRoot: Fr, feeAssetPriceModifier: bigint): Hex {
483
+ return computeCheckpointPayloadDigest({
484
+ header,
485
+ archiveRoot,
486
+ feeAssetPriceModifier,
487
+ signatureContext: this.getSignatureContext(),
488
+ }).toString();
489
+ }
490
+
491
+ /**
492
+ * Extracts the CommitteeAttestations struct definition from RollupAbi.
493
+ * Finds the _attestations parameter by name in the propose function.
494
+ */
495
+ private getCommitteeAttestationsStructDef(): AbiParameter {
496
+ const proposeFunction = RollupAbi.find(item => item.type === 'function' && item.name === 'propose') as
497
+ | { type: 'function'; name: string; inputs: readonly AbiParameter[] }
498
+ | undefined;
499
+
500
+ if (!proposeFunction) {
501
+ throw new Error('propose function not found in RollupAbi');
502
+ }
503
+
504
+ // Find the _attestations parameter by name, not by index
505
+ const attestationsParam = proposeFunction.inputs.find(param => param.name === '_attestations');
506
+
507
+ if (!attestationsParam) {
508
+ throw new Error('_attestations parameter not found in propose function');
509
+ }
510
+
511
+ if (attestationsParam.type !== 'tuple') {
512
+ throw new Error(`Expected _attestations parameter to be a tuple, got ${attestationsParam.type}`);
513
+ }
514
+
515
+ // Extract the tuple components (struct fields)
516
+ const tupleParam = attestationsParam as unknown as {
517
+ type: 'tuple';
518
+ components?: readonly AbiParameter[];
519
+ };
520
+
521
+ return {
522
+ type: 'tuple',
523
+ components: tupleParam.components || [],
524
+ } as AbiParameter;
525
+ }
526
+ }
527
+
528
+ /** Function selector for the `propose` method of the rollup contract. */
529
+ const PROPOSE_SELECTOR = toFunctionSelector(RollupAbi.find(x => x.type === 'function' && x.name === 'propose')!);