@aztec/archiver 0.0.1-commit.e2b2873ed → 0.0.1-commit.e304674f1

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