@aztec/archiver 0.0.1-commit.96bb3f7 → 0.0.1-commit.993d240

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 (232) hide show
  1. package/README.md +164 -22
  2. package/dest/archiver.d.ts +158 -0
  3. package/dest/archiver.d.ts.map +1 -0
  4. package/dest/archiver.js +881 -0
  5. package/dest/config.d.ts +33 -0
  6. package/dest/config.d.ts.map +1 -0
  7. package/dest/{archiver/config.js → config.js} +31 -14
  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 +112 -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/{archiver/l1 → l1}/bin/retrieve-calldata.d.ts +1 -1
  21. package/dest/l1/bin/retrieve-calldata.d.ts.map +1 -0
  22. package/dest/{archiver/l1 → l1}/bin/retrieve-calldata.js +35 -32
  23. package/dest/l1/calldata_retriever.d.ts +136 -0
  24. package/dest/l1/calldata_retriever.d.ts.map +1 -0
  25. package/dest/l1/calldata_retriever.js +412 -0
  26. package/dest/l1/data_retrieval.d.ts +97 -0
  27. package/dest/l1/data_retrieval.d.ts.map +1 -0
  28. package/dest/{archiver/l1 → l1}/data_retrieval.js +65 -89
  29. package/dest/{archiver/l1 → l1}/debug_tx.d.ts +1 -1
  30. package/dest/l1/debug_tx.d.ts.map +1 -0
  31. package/dest/{archiver/l1 → l1}/spire_proposer.d.ts +5 -5
  32. package/dest/l1/spire_proposer.d.ts.map +1 -0
  33. package/dest/{archiver/l1 → l1}/spire_proposer.js +9 -17
  34. package/dest/l1/trace_tx.d.ts +43 -0
  35. package/dest/l1/trace_tx.d.ts.map +1 -0
  36. package/dest/l1/types.d.ts +12 -0
  37. package/dest/l1/types.d.ts.map +1 -0
  38. package/dest/l1/validate_historical_logs.d.ts +23 -0
  39. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  40. package/dest/l1/validate_historical_logs.js +108 -0
  41. package/dest/{archiver/l1 → l1}/validate_trace.d.ts +6 -3
  42. package/dest/l1/validate_trace.d.ts.map +1 -0
  43. package/dest/{archiver/l1 → l1}/validate_trace.js +13 -9
  44. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  45. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  46. package/dest/modules/contract_data_source_adapter.js +40 -0
  47. package/dest/modules/data_source_base.d.ts +113 -0
  48. package/dest/modules/data_source_base.d.ts.map +1 -0
  49. package/dest/modules/data_source_base.js +351 -0
  50. package/dest/modules/data_store_updater.d.ts +105 -0
  51. package/dest/modules/data_store_updater.d.ts.map +1 -0
  52. package/dest/modules/data_store_updater.js +392 -0
  53. package/dest/modules/instrumentation.d.ts +55 -0
  54. package/dest/modules/instrumentation.d.ts.map +1 -0
  55. package/dest/{archiver → modules}/instrumentation.js +61 -19
  56. package/dest/modules/l1_synchronizer.d.ts +77 -0
  57. package/dest/modules/l1_synchronizer.d.ts.map +1 -0
  58. package/dest/modules/l1_synchronizer.js +1344 -0
  59. package/dest/modules/validation.d.ts +18 -0
  60. package/dest/modules/validation.d.ts.map +1 -0
  61. package/dest/{archiver → modules}/validation.js +12 -6
  62. package/dest/store/block_store.d.ts +300 -0
  63. package/dest/store/block_store.d.ts.map +1 -0
  64. package/dest/store/block_store.js +1219 -0
  65. package/dest/store/contract_class_store.d.ts +31 -0
  66. package/dest/store/contract_class_store.d.ts.map +1 -0
  67. package/dest/store/contract_class_store.js +80 -0
  68. package/dest/store/contract_instance_store.d.ts +51 -0
  69. package/dest/store/contract_instance_store.d.ts.map +1 -0
  70. package/dest/{archiver/kv_archiver_store → store}/contract_instance_store.js +38 -3
  71. package/dest/store/data_stores.d.ts +68 -0
  72. package/dest/store/data_stores.d.ts.map +1 -0
  73. package/dest/store/data_stores.js +54 -0
  74. package/dest/store/function_names_cache.d.ts +17 -0
  75. package/dest/store/function_names_cache.d.ts.map +1 -0
  76. package/dest/store/function_names_cache.js +30 -0
  77. package/dest/store/l2_tips_cache.d.ts +25 -0
  78. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  79. package/dest/store/l2_tips_cache.js +26 -0
  80. package/dest/store/log_store.d.ts +59 -0
  81. package/dest/store/log_store.d.ts.map +1 -0
  82. package/dest/store/log_store.js +310 -0
  83. package/dest/store/log_store_codec.d.ts +70 -0
  84. package/dest/store/log_store_codec.d.ts.map +1 -0
  85. package/dest/store/log_store_codec.js +101 -0
  86. package/dest/store/message_store.d.ts +50 -0
  87. package/dest/store/message_store.d.ts.map +1 -0
  88. package/dest/{archiver/kv_archiver_store → store}/message_store.js +51 -9
  89. package/dest/{archiver/structs → structs}/data_retrieval.d.ts +1 -1
  90. package/dest/structs/data_retrieval.d.ts.map +1 -0
  91. package/dest/structs/inbox_message.d.ts +15 -0
  92. package/dest/structs/inbox_message.d.ts.map +1 -0
  93. package/dest/{archiver/structs → structs}/published.d.ts +1 -1
  94. package/dest/structs/published.d.ts.map +1 -0
  95. package/dest/test/fake_l1_state.d.ts +214 -0
  96. package/dest/test/fake_l1_state.d.ts.map +1 -0
  97. package/dest/test/fake_l1_state.js +517 -0
  98. package/dest/test/index.d.ts +2 -1
  99. package/dest/test/index.d.ts.map +1 -1
  100. package/dest/test/index.js +4 -1
  101. package/dest/test/mock_archiver.d.ts +2 -2
  102. package/dest/test/mock_archiver.d.ts.map +1 -1
  103. package/dest/test/mock_archiver.js +3 -3
  104. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  105. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  106. package/dest/test/mock_l1_to_l2_message_source.js +2 -1
  107. package/dest/test/mock_l2_block_source.d.ts +65 -41
  108. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  109. package/dest/test/mock_l2_block_source.js +330 -151
  110. package/dest/test/mock_structs.d.ts +81 -3
  111. package/dest/test/mock_structs.d.ts.map +1 -1
  112. package/dest/test/mock_structs.js +152 -7
  113. package/dest/test/noop_l1_archiver.d.ts +29 -0
  114. package/dest/test/noop_l1_archiver.d.ts.map +1 -0
  115. package/dest/test/noop_l1_archiver.js +85 -0
  116. package/package.json +17 -18
  117. package/src/archiver.ts +681 -0
  118. package/src/{archiver/config.ts → config.ts} +43 -12
  119. package/src/errors.ts +203 -0
  120. package/src/factory.ts +175 -22
  121. package/src/index.ts +27 -3
  122. package/src/interfaces.ts +9 -0
  123. package/src/l1/README.md +55 -0
  124. package/src/{archiver/l1 → l1}/bin/retrieve-calldata.ts +45 -33
  125. package/src/l1/calldata_retriever.ts +522 -0
  126. package/src/{archiver/l1 → l1}/data_retrieval.ts +106 -134
  127. package/src/{archiver/l1 → l1}/spire_proposer.ts +7 -15
  128. package/src/l1/validate_historical_logs.ts +140 -0
  129. package/src/{archiver/l1 → l1}/validate_trace.ts +24 -6
  130. package/src/modules/contract_data_source_adapter.ts +55 -0
  131. package/src/modules/data_source_base.ts +493 -0
  132. package/src/modules/data_store_updater.ts +518 -0
  133. package/src/{archiver → modules}/instrumentation.ts +72 -20
  134. package/src/modules/l1_synchronizer.ts +1257 -0
  135. package/src/{archiver → modules}/validation.ts +15 -9
  136. package/src/store/block_store.ts +1590 -0
  137. package/src/store/contract_class_store.ts +108 -0
  138. package/src/{archiver/kv_archiver_store → store}/contract_instance_store.ts +52 -6
  139. package/src/store/data_stores.ts +104 -0
  140. package/src/store/function_names_cache.ts +37 -0
  141. package/src/store/l2_tips_cache.ts +35 -0
  142. package/src/store/log_store.ts +379 -0
  143. package/src/store/log_store_codec.ts +132 -0
  144. package/src/{archiver/kv_archiver_store → store}/message_store.ts +60 -10
  145. package/src/{archiver/structs → structs}/inbox_message.ts +1 -1
  146. package/src/test/fake_l1_state.ts +770 -0
  147. package/src/test/index.ts +4 -0
  148. package/src/test/mock_archiver.ts +4 -3
  149. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  150. package/src/test/mock_l2_block_source.ts +403 -171
  151. package/src/test/mock_structs.ts +283 -8
  152. package/src/test/noop_l1_archiver.ts +139 -0
  153. package/dest/archiver/archiver.d.ts +0 -307
  154. package/dest/archiver/archiver.d.ts.map +0 -1
  155. package/dest/archiver/archiver.js +0 -2102
  156. package/dest/archiver/archiver_store.d.ts +0 -315
  157. package/dest/archiver/archiver_store.d.ts.map +0 -1
  158. package/dest/archiver/archiver_store.js +0 -4
  159. package/dest/archiver/archiver_store_test_suite.d.ts +0 -8
  160. package/dest/archiver/archiver_store_test_suite.d.ts.map +0 -1
  161. package/dest/archiver/archiver_store_test_suite.js +0 -2770
  162. package/dest/archiver/config.d.ts +0 -22
  163. package/dest/archiver/config.d.ts.map +0 -1
  164. package/dest/archiver/errors.d.ts +0 -36
  165. package/dest/archiver/errors.d.ts.map +0 -1
  166. package/dest/archiver/errors.js +0 -54
  167. package/dest/archiver/index.d.ts +0 -7
  168. package/dest/archiver/index.d.ts.map +0 -1
  169. package/dest/archiver/index.js +0 -4
  170. package/dest/archiver/instrumentation.d.ts +0 -37
  171. package/dest/archiver/instrumentation.d.ts.map +0 -1
  172. package/dest/archiver/kv_archiver_store/block_store.d.ts +0 -164
  173. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +0 -1
  174. package/dest/archiver/kv_archiver_store/block_store.js +0 -626
  175. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +0 -18
  176. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +0 -1
  177. package/dest/archiver/kv_archiver_store/contract_class_store.js +0 -120
  178. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +0 -24
  179. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +0 -1
  180. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +0 -159
  181. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +0 -1
  182. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +0 -316
  183. package/dest/archiver/kv_archiver_store/log_store.d.ts +0 -45
  184. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +0 -1
  185. package/dest/archiver/kv_archiver_store/log_store.js +0 -401
  186. package/dest/archiver/kv_archiver_store/message_store.d.ts +0 -40
  187. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +0 -1
  188. package/dest/archiver/l1/bin/retrieve-calldata.d.ts.map +0 -1
  189. package/dest/archiver/l1/calldata_retriever.d.ts +0 -112
  190. package/dest/archiver/l1/calldata_retriever.d.ts.map +0 -1
  191. package/dest/archiver/l1/calldata_retriever.js +0 -471
  192. package/dest/archiver/l1/data_retrieval.d.ts +0 -90
  193. package/dest/archiver/l1/data_retrieval.d.ts.map +0 -1
  194. package/dest/archiver/l1/debug_tx.d.ts.map +0 -1
  195. package/dest/archiver/l1/spire_proposer.d.ts.map +0 -1
  196. package/dest/archiver/l1/trace_tx.d.ts +0 -97
  197. package/dest/archiver/l1/trace_tx.d.ts.map +0 -1
  198. package/dest/archiver/l1/types.d.ts +0 -12
  199. package/dest/archiver/l1/types.d.ts.map +0 -1
  200. package/dest/archiver/l1/validate_trace.d.ts.map +0 -1
  201. package/dest/archiver/structs/data_retrieval.d.ts.map +0 -1
  202. package/dest/archiver/structs/inbox_message.d.ts +0 -15
  203. package/dest/archiver/structs/inbox_message.d.ts.map +0 -1
  204. package/dest/archiver/structs/published.d.ts.map +0 -1
  205. package/dest/archiver/validation.d.ts +0 -17
  206. package/dest/archiver/validation.d.ts.map +0 -1
  207. package/dest/rpc/index.d.ts +0 -9
  208. package/dest/rpc/index.d.ts.map +0 -1
  209. package/dest/rpc/index.js +0 -15
  210. package/src/archiver/archiver.ts +0 -2265
  211. package/src/archiver/archiver_store.ts +0 -380
  212. package/src/archiver/archiver_store_test_suite.ts +0 -2842
  213. package/src/archiver/errors.ts +0 -90
  214. package/src/archiver/index.ts +0 -6
  215. package/src/archiver/kv_archiver_store/block_store.ts +0 -850
  216. package/src/archiver/kv_archiver_store/contract_class_store.ts +0 -176
  217. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +0 -442
  218. package/src/archiver/kv_archiver_store/log_store.ts +0 -516
  219. package/src/archiver/l1/README.md +0 -98
  220. package/src/archiver/l1/calldata_retriever.ts +0 -641
  221. package/src/rpc/index.ts +0 -16
  222. /package/dest/{archiver/l1 → l1}/debug_tx.js +0 -0
  223. /package/dest/{archiver/l1 → l1}/trace_tx.js +0 -0
  224. /package/dest/{archiver/l1 → l1}/types.js +0 -0
  225. /package/dest/{archiver/structs → structs}/data_retrieval.js +0 -0
  226. /package/dest/{archiver/structs → structs}/inbox_message.js +0 -0
  227. /package/dest/{archiver/structs → structs}/published.js +0 -0
  228. /package/src/{archiver/l1 → l1}/debug_tx.ts +0 -0
  229. /package/src/{archiver/l1 → l1}/trace_tx.ts +0 -0
  230. /package/src/{archiver/l1 → l1}/types.ts +0 -0
  231. /package/src/{archiver/structs → structs}/data_retrieval.ts +0 -0
  232. /package/src/{archiver/structs → structs}/published.ts +0 -0
@@ -0,0 +1,770 @@
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
+ import { type Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
+ import { INITIAL_CHECKPOINT_NUMBER } from '@aztec/constants';
4
+ import type { CheckpointProposedLog, InboxContract, MessageSentLog, RollupContract } from '@aztec/ethereum/contracts';
5
+ import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum/contracts';
6
+ import type { ViemPublicClient } from '@aztec/ethereum/types';
7
+ import { type BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
8
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
9
+ import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
10
+ import { Fr } from '@aztec/foundation/curves/bn254';
11
+ import { EthAddress } from '@aztec/foundation/eth-address';
12
+ import { createLogger } from '@aztec/foundation/log';
13
+ import { RollupAbi } from '@aztec/l1-artifacts';
14
+ import { CommitteeAttestation, CommitteeAttestationsAndSigners, L2Block } from '@aztec/stdlib/block';
15
+ import { Checkpoint } from '@aztec/stdlib/checkpoint';
16
+ import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
17
+ import { InboxLeaf } from '@aztec/stdlib/messaging';
18
+ import { ConsensusPayload, getHashedSignaturePayloadTypedData } from '@aztec/stdlib/p2p';
19
+ import { mockCheckpointAndMessages } from '@aztec/stdlib/testing';
20
+ import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
21
+
22
+ import { type MockProxy, mock } from 'jest-mock-extended';
23
+ import {
24
+ type AbiParameter,
25
+ type FormattedBlock,
26
+ type Transaction,
27
+ encodeAbiParameters,
28
+ encodeFunctionData,
29
+ keccak256,
30
+ multicall3Abi,
31
+ toHex,
32
+ } from 'viem';
33
+
34
+ import { updateRollingHash } from '../structs/inbox_message.js';
35
+
36
+ /** Configuration for the fake L1 state. */
37
+ export type FakeL1StateConfig = {
38
+ /** Genesis archive root. */
39
+ genesisArchiveRoot: Fr;
40
+ /** L1 start block number. */
41
+ l1StartBlock: bigint;
42
+ /** L1 genesis time in seconds. */
43
+ l1GenesisTime: bigint;
44
+ /** Ethereum slot duration in seconds. */
45
+ ethereumSlotDuration: number;
46
+ /** Rollup address for mock contracts. */
47
+ rollupAddress: EthAddress;
48
+ /** Inbox address for mock contracts. */
49
+ inboxAddress: EthAddress;
50
+ /** Aztec slot duration in seconds */
51
+ slotDuration: number;
52
+ };
53
+
54
+ /** Options for adding a checkpoint. */
55
+ type AddCheckpointOptions = {
56
+ /** L1 block number where checkpoint was proposed. */
57
+ l1BlockNumber: bigint;
58
+ /** Number of L2 blocks in the checkpoint. Default: 1 */
59
+ numBlocks?: number;
60
+ /** Or the actual blocks for the checkpoint */
61
+ blocks?: L2Block[];
62
+ /** Number of transactions per block. Default: 4 */
63
+ txsPerBlock?: number;
64
+ /** Max number of effects per tx (for generating large blobs). Default: undefined */
65
+ maxEffects?: number;
66
+ /** Signers for attestations. Default: none */
67
+ signers?: Secp256k1Signer[];
68
+ /** Override slot number. */
69
+ slotNumber?: SlotNumber;
70
+ /** Override previous archive. */
71
+ previousArchive?: AppendOnlyTreeSnapshot;
72
+ /** Timestamp for the checkpoint. */
73
+ timestamp?: bigint;
74
+ /** Number of L1-to-L2 messages. Default: 3 */
75
+ numL1ToL2Messages?: number;
76
+ /** L1 block number where messages were sent. Default: l1BlockNumber - 3 */
77
+ messagesL1BlockNumber?: bigint;
78
+ };
79
+
80
+ /** Result from adding a checkpoint. */
81
+ type AddCheckpointResult = {
82
+ /** The checkpoint that was created. */
83
+ checkpoint: Checkpoint;
84
+ /** The L1-to-L2 messages for this checkpoint. */
85
+ messages: Fr[];
86
+ };
87
+
88
+ /** Data stored for a checkpoint. */
89
+ type CheckpointData = {
90
+ checkpointNumber: CheckpointNumber;
91
+ checkpoint: Checkpoint;
92
+ l1BlockNumber: bigint;
93
+ tx: Transaction;
94
+ blobHashes: `0x${string}`[];
95
+ blobs: Blob[];
96
+ signers: Secp256k1Signer[];
97
+ /** Hash of the packed attestations, matching what the L1 event emits. */
98
+ attestationsHash: Buffer32;
99
+ /** Payload digest, matching what the L1 event emits. */
100
+ payloadDigest: Buffer32;
101
+ /** If true, archiveAt will ignore it */
102
+ pruned?: boolean;
103
+ };
104
+
105
+ /** Data stored for a message. */
106
+ type MessageData = {
107
+ l1BlockNumber: bigint;
108
+ checkpointNumber: CheckpointNumber;
109
+ index: bigint;
110
+ leaf: Fr;
111
+ rollingHash: Buffer16;
112
+ };
113
+
114
+ /**
115
+ * Stateful fake for L1 data used by the archiver.
116
+ *
117
+ * This class simulates the L1 blockchain state that the archiver reads from:
118
+ * - RollupContract: status(), archiveAt(), getVersion(), getTargetCommitteeSize(), CheckpointProposed events
119
+ * - InboxContract: getState(), MessageSent events
120
+ * - PublicClient: getBlockNumber(), getBlock(), getTransaction()
121
+ * - BlobClient: getBlobSidecar()
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const fake = new FakeL1State(config);
126
+ *
127
+ * // Add checkpoint (creates messages automatically)
128
+ * const { checkpoint, messages } = await fake.addCheckpoint(CheckpointNumber(1), { l1BlockNumber: 101n });
129
+ * fake.setL1BlockNumber(105n);
130
+ *
131
+ * // Status auto-updated
132
+ * expect(fake.getRollupStatus().pendingCheckpointNumber).toEqual(CheckpointNumber(1));
133
+ * ```
134
+ */
135
+ export class FakeL1State {
136
+ private readonly log = createLogger('archiver:test:fake-l1');
137
+ private l1BlockNumber: bigint;
138
+ private checkpoints: CheckpointData[] = [];
139
+ private messages: MessageData[] = [];
140
+ private messagesRollingHash: Buffer16 = Buffer16.ZERO;
141
+ private lastArchive: AppendOnlyTreeSnapshot;
142
+ private provenCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
143
+ private targetCommitteeSize: number = 0;
144
+ private version: bigint = 1n;
145
+ private canPruneResult: boolean = false;
146
+
147
+ // Computed from checkpoints based on L1 block visibility
148
+ private pendingCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
149
+
150
+ // The L1 block number reported as "finalized" (defaults to the start block).
151
+ // `undefined` simulates the startup window on a fresh devnet where
152
+ // `getBlock({ blockTag: 'finalized' })` fails with "finalized block not found".
153
+ private finalizedL1BlockNumber: bigint | undefined;
154
+
155
+ constructor(private readonly config: FakeL1StateConfig) {
156
+ this.l1BlockNumber = config.l1StartBlock;
157
+ this.finalizedL1BlockNumber = config.l1StartBlock;
158
+ this.lastArchive = new AppendOnlyTreeSnapshot(config.genesisArchiveRoot, 1);
159
+ }
160
+
161
+ /**
162
+ * Adds messages for a checkpoint. Returns the message leaves.
163
+ * Auto-updates rolling hash.
164
+ *
165
+ * Note: For most use cases, use `addCheckpoint` which creates both checkpoint and messages.
166
+ * Use this method only when you need to add messages without creating a checkpoint (e.g., for reorg tests).
167
+ */
168
+ addMessages(checkpointNumber: CheckpointNumber, l1BlockNumber: bigint, messageLeaves: Fr[]): void {
169
+ messageLeaves.forEach((leaf, i) => {
170
+ const index = InboxLeaf.smallestIndexForCheckpoint(checkpointNumber) + BigInt(i);
171
+ this.messagesRollingHash = updateRollingHash(this.messagesRollingHash, leaf);
172
+
173
+ this.messages.push({
174
+ l1BlockNumber,
175
+ checkpointNumber,
176
+ index,
177
+ leaf,
178
+ rollingHash: this.messagesRollingHash,
179
+ });
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Creates blocks for a checkpoint without adding them to L1 state.
185
+ * Useful for creating blocks to pass to addBlock() for testing provisional block handling.
186
+ * Returns the blocks directly.
187
+ */
188
+ public async makeBlocks(checkpointNumber: CheckpointNumber, options: Partial<AddCheckpointOptions>) {
189
+ return (await this.makeCheckpointAndMessages(checkpointNumber, options)).checkpoint.blocks;
190
+ }
191
+
192
+ /**
193
+ * Creates and adds a checkpoint with its L1-to-L2 messages.
194
+ * Returns both the checkpoint and the message leaves.
195
+ * Auto-chains from lastArchive, auto-updates pending status if L1 block >= checkpoint's L1 block.
196
+ */
197
+ public async addCheckpoint(
198
+ checkpointNumber: CheckpointNumber,
199
+ options: AddCheckpointOptions,
200
+ ): Promise<AddCheckpointResult> {
201
+ this.log.warn(`Adding checkpoint ${checkpointNumber}`);
202
+
203
+ const { l1BlockNumber, signers = [], messagesL1BlockNumber = l1BlockNumber - 3n } = options;
204
+
205
+ // Create the checkpoint using the stdlib helper
206
+ const { checkpoint, messages, lastArchive } = await this.makeCheckpointAndMessages(checkpointNumber, {
207
+ ...options,
208
+ numL1ToL2Messages: options.numL1ToL2Messages ?? 3,
209
+ });
210
+
211
+ // Store the messages internally so they match the checkpoint's inHash
212
+ this.addMessages(checkpointNumber, messagesL1BlockNumber, messages);
213
+
214
+ // Create the transaction, blobs, and event hashes
215
+ const { tx, attestationsHash, payloadDigest } = await this.makeRollupTx(checkpoint, signers);
216
+ const blobHashes = await this.makeVersionedBlobHashes(checkpoint);
217
+ const blobs = await this.makeBlobsFromCheckpoint(checkpoint);
218
+
219
+ // Store the checkpoint data
220
+ this.checkpoints.push({
221
+ checkpointNumber,
222
+ checkpoint,
223
+ l1BlockNumber,
224
+ tx,
225
+ blobHashes,
226
+ blobs,
227
+ signers,
228
+ attestationsHash,
229
+ payloadDigest,
230
+ });
231
+
232
+ // Update last archive for auto-chaining
233
+ this.lastArchive = lastArchive ?? checkpoint.blocks.at(-1)!.archive;
234
+
235
+ // Auto-update pending checkpoint number
236
+ this.updatePendingCheckpointNumber();
237
+
238
+ return { checkpoint, messages };
239
+ }
240
+
241
+ /** Creates a checkpoint and messages without adding them to L1 state. */
242
+ private makeCheckpointAndMessages(checkpointNumber: CheckpointNumber, options: Partial<AddCheckpointOptions>) {
243
+ const {
244
+ numBlocks = 1,
245
+ txsPerBlock = 4,
246
+ maxEffects,
247
+ slotNumber,
248
+ previousArchive = this.lastArchive,
249
+ timestamp,
250
+ l1BlockNumber,
251
+ numL1ToL2Messages = 0,
252
+ blocks,
253
+ } = options;
254
+
255
+ return mockCheckpointAndMessages(checkpointNumber, {
256
+ startBlockNumber: this.getNextBlockNumber(checkpointNumber),
257
+ numBlocks,
258
+ blocks,
259
+ numTxsPerBlock: txsPerBlock,
260
+ numL1ToL2Messages,
261
+ previousArchive,
262
+ slotNumber: slotNumber ?? (l1BlockNumber !== undefined ? this.getL2SlotAtL1Block(l1BlockNumber) : undefined),
263
+ timestamp: timestamp ?? (l1BlockNumber !== undefined ? this.getTimestampAtL1Block(l1BlockNumber) : undefined),
264
+ ...(maxEffects !== undefined ? { maxEffects } : {}),
265
+ });
266
+ }
267
+
268
+ /** Returns the L2 slot at the given L1 block (assuming all L1 blocks are mined) */
269
+ public getL2SlotAtL1Block(l1BlockNumber: bigint): SlotNumber {
270
+ const timestamp = this.getTimestampAtL1Block(l1BlockNumber);
271
+ return getSlotAtTimestamp(timestamp, this.config);
272
+ }
273
+
274
+ /** Returns the timestamp at the given L1 block (assuming all L1 blocks are mined) */
275
+ public getTimestampAtL1Block(l1BlockNumber: bigint): bigint {
276
+ const { l1GenesisTime, l1StartBlock, ethereumSlotDuration } = this.config;
277
+ return l1GenesisTime + (l1BlockNumber - l1StartBlock) * BigInt(ethereumSlotDuration);
278
+ }
279
+
280
+ /**
281
+ * Sets the current L1 block number.
282
+ * Auto-updates pending checkpoint number based on visible checkpoints.
283
+ */
284
+ setL1BlockNumber(blockNumber: bigint): void {
285
+ this.l1BlockNumber = blockNumber;
286
+ this.updatePendingCheckpointNumber();
287
+ }
288
+
289
+ /**
290
+ * Sets the L1 block number that will be reported as "finalized". Pass `undefined` to
291
+ * simulate a chain that does not yet have a finalized block (devnet startup).
292
+ */
293
+ setFinalizedL1BlockNumber(blockNumber: bigint | undefined): void {
294
+ this.finalizedL1BlockNumber = blockNumber;
295
+ }
296
+
297
+ /** Marks a checkpoint as proven. Updates provenCheckpointNumber. */
298
+ markCheckpointAsProven(checkpointNumber: CheckpointNumber): void {
299
+ this.provenCheckpointNumber = checkpointNumber;
300
+ }
301
+
302
+ /**
303
+ * Simulates what `rollup.getProvenCheckpointNumber({ blockNumber: atL1Block })` would return.
304
+ */
305
+ getProvenCheckpointNumberAtL1Block(atL1Block: bigint): CheckpointNumber {
306
+ if (this.provenCheckpointNumber === 0) {
307
+ return CheckpointNumber(0);
308
+ }
309
+ const checkpoint = this.checkpoints.find(cp => cp.checkpointNumber === this.provenCheckpointNumber);
310
+ if (checkpoint && checkpoint.l1BlockNumber <= atL1Block) {
311
+ return this.provenCheckpointNumber;
312
+ }
313
+ return CheckpointNumber(0);
314
+ }
315
+
316
+ /** Sets the target committee size for attestation validation. */
317
+ setTargetCommitteeSize(size: number): void {
318
+ this.targetCommitteeSize = size;
319
+ }
320
+
321
+ /** Sets whether the rollup contract would allow pruning at the next block. */
322
+ setCanPrune(value: boolean): void {
323
+ this.canPruneResult = value;
324
+ }
325
+
326
+ /**
327
+ * Removes all entries for a checkpoint number (simulates L1 reorg or prune).
328
+ * Note: Does NOT remove messages for this checkpoint (use numL1ToL2Messages: 0 when re-adding).
329
+ * Auto-updates pending status.
330
+ */
331
+ removeCheckpoint(checkpointNumber: CheckpointNumber): void {
332
+ this.checkpoints = this.checkpoints.filter(cpData => cpData.checkpointNumber !== checkpointNumber);
333
+ this.updatePendingCheckpointNumber();
334
+ }
335
+
336
+ /**
337
+ * Moves a checkpoint to a different L1 block number (simulates L1 reorg that
338
+ * re-includes the same checkpoint transaction in a different block).
339
+ * The checkpoint content stays the same — only the L1 metadata changes.
340
+ * Auto-updates pending status.
341
+ */
342
+ moveCheckpointToL1Block(checkpointNumber: CheckpointNumber, newL1BlockNumber: bigint): void {
343
+ for (const cpData of this.checkpoints) {
344
+ if (cpData.checkpointNumber === checkpointNumber) {
345
+ cpData.l1BlockNumber = newL1BlockNumber;
346
+ }
347
+ }
348
+ this.updatePendingCheckpointNumber();
349
+ }
350
+
351
+ /**
352
+ * Removes messages after a given total index (simulates L1 reorg).
353
+ * Auto-updates rolling hash.
354
+ */
355
+ removeMessagesAfter(totalIndex: number): void {
356
+ this.messages = this.messages.slice(0, totalIndex);
357
+ // Recalculate rolling hash
358
+ this.messagesRollingHash = this.messages.reduce((hash, msg) => updateRollingHash(hash, msg.leaf), Buffer16.ZERO);
359
+ }
360
+
361
+ /**
362
+ * Simulates a pruned checkpoint by marking all entries with this number as pruned.
363
+ * The event will still be returned, but archiveAt() will return the previous checkpoint's archive
364
+ * (or genesis if no previous checkpoint), causing a mismatch that the archiver will detect.
365
+ */
366
+ markCheckpointAsPruned(checkpointNumber: CheckpointNumber): void {
367
+ for (const cpData of this.checkpoints) {
368
+ if (cpData.checkpointNumber === checkpointNumber) {
369
+ cpData.pruned = true;
370
+ }
371
+ }
372
+ this.updatePendingCheckpointNumber();
373
+ }
374
+
375
+ /**
376
+ * Moves messages at a given L1 block to a new L1 block.
377
+ * Useful for simulating partial message visibility (messages at higher L1 blocks won't be fetched).
378
+ */
379
+ moveMessagesToL1Block(fromL1Block: bigint, toL1Block: bigint): void {
380
+ this.messages.forEach(msg => {
381
+ if (msg.l1BlockNumber === fromL1Block) {
382
+ msg.l1BlockNumber = toL1Block;
383
+ }
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Moves a specific message (by index in the messages array) to a new L1 block.
389
+ */
390
+ moveMessageAtIndexToL1Block(index: number, toL1Block: bigint): void {
391
+ if (this.messages[index]) {
392
+ this.messages[index].l1BlockNumber = toL1Block;
393
+ }
394
+ }
395
+
396
+ /** Gets current rollup status. */
397
+ getRollupStatus(): {
398
+ provenCheckpointNumber: CheckpointNumber;
399
+ pendingCheckpointNumber: CheckpointNumber;
400
+ provenArchive: Fr;
401
+ pendingArchive: Fr;
402
+ } {
403
+ return {
404
+ provenCheckpointNumber: this.provenCheckpointNumber,
405
+ pendingCheckpointNumber: this.pendingCheckpointNumber,
406
+ provenArchive: this.getArchiveAt(this.provenCheckpointNumber),
407
+ pendingArchive: this.getArchiveAt(this.pendingCheckpointNumber),
408
+ };
409
+ }
410
+
411
+ /** Gets the last archive (for manual chaining if needed). */
412
+ getLastArchive(): AppendOnlyTreeSnapshot {
413
+ return this.lastArchive;
414
+ }
415
+
416
+ /** Gets the latest checkpoint entry by number (returns the last added one). */
417
+ getCheckpoint(checkpointNumber: CheckpointNumber): Checkpoint | undefined {
418
+ return this.checkpoints.findLast(cpData => cpData.checkpointNumber === checkpointNumber)?.checkpoint;
419
+ }
420
+
421
+ /** Gets messages for a checkpoint. */
422
+ getMessages(checkpointNumber: CheckpointNumber): Fr[] {
423
+ return this.messages.filter(m => m.checkpointNumber === checkpointNumber).map(m => m.leaf);
424
+ }
425
+
426
+ /** Gets the blobs for a checkpoint. */
427
+ getCheckpointBlobs(checkpointNumber: CheckpointNumber): Blob[] {
428
+ return this.checkpoints.findLast(cpData => cpData.checkpointNumber === checkpointNumber)?.blobs ?? [];
429
+ }
430
+
431
+ /** Creates mock RollupContract that reads from this fake state. */
432
+ createMockRollupContract(_publicClient: MockProxy<ViemPublicClient>): MockProxy<RollupContract> {
433
+ const mockRollup = mock<RollupContract>();
434
+
435
+ mockRollup.getVersion.mockImplementation(() => Promise.resolve(this.version));
436
+ mockRollup.getTargetCommitteeSize.mockImplementation(() => Promise.resolve(this.targetCommitteeSize));
437
+ mockRollup.archiveAt.mockImplementation((cpNum: CheckpointNumber) => Promise.resolve(this.getArchiveAt(cpNum)));
438
+ mockRollup.status.mockImplementation((localCheckpointNum: CheckpointNumber) => {
439
+ const status = this.getRollupStatus();
440
+ return Promise.resolve({
441
+ provenCheckpointNumber: status.provenCheckpointNumber,
442
+ provenArchive: status.provenArchive,
443
+ pendingCheckpointNumber: status.pendingCheckpointNumber,
444
+ pendingArchive: status.pendingArchive,
445
+ archiveOfMyCheckpoint: this.getArchiveAt(localCheckpointNum),
446
+ });
447
+ });
448
+
449
+ mockRollup.getProvenCheckpointNumber.mockImplementation((options?: { blockNumber?: bigint }) => {
450
+ const atBlock = options?.blockNumber ?? this.l1BlockNumber;
451
+ return Promise.resolve(this.getProvenCheckpointNumberAtL1Block(atBlock));
452
+ });
453
+
454
+ mockRollup.canPruneAtTime.mockImplementation(() => Promise.resolve(this.canPruneResult));
455
+
456
+ // Mock the wrapper method for fetching checkpoint events
457
+ mockRollup.getCheckpointProposedEvents.mockImplementation((fromBlock: bigint, toBlock: bigint) =>
458
+ Promise.resolve(this.getCheckpointProposedLogs(fromBlock, toBlock)),
459
+ );
460
+
461
+ Object.defineProperty(mockRollup, 'address', { get: () => this.config.rollupAddress.toString() });
462
+
463
+ return mockRollup;
464
+ }
465
+
466
+ /** Creates mock InboxContract that reads from this fake state. */
467
+ createMockInboxContract(_publicClient: MockProxy<ViemPublicClient>): MockProxy<InboxContract> {
468
+ const mockInbox = mock<InboxContract>();
469
+
470
+ mockInbox.getState.mockImplementation((opts: { blockTag?: string; blockNumber?: bigint } = {}) => {
471
+ // Filter messages visible at the given block number (or all if not specified)
472
+ const blockNumber = opts.blockNumber ?? this.l1BlockNumber;
473
+ const visibleMessages = this.messages.filter(m => m.l1BlockNumber <= blockNumber);
474
+
475
+ // treeInProgress must be > any sealed checkpoint. On L1, a checkpoint can only be proposed
476
+ // after its messages are sealed, so treeInProgress > checkpointNumber for all published checkpoints.
477
+ const maxFromMessages =
478
+ visibleMessages.length > 0 ? Math.max(...visibleMessages.map(m => Number(m.checkpointNumber))) + 1 : 0;
479
+ const maxFromCheckpoints =
480
+ this.checkpoints.length > 0
481
+ ? Math.max(
482
+ ...this.checkpoints
483
+ .filter(cp => !cp.pruned && cp.l1BlockNumber <= blockNumber)
484
+ .map(cp => Number(cp.checkpointNumber)),
485
+ 0,
486
+ ) + 1
487
+ : 0;
488
+ const treeInProgress = Math.max(maxFromMessages, maxFromCheckpoints, INITIAL_CHECKPOINT_NUMBER);
489
+
490
+ // Compute rolling hash only for visible messages
491
+ const rollingHash =
492
+ visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1].rollingHash : Buffer16.ZERO;
493
+
494
+ return Promise.resolve({
495
+ messagesRollingHash: rollingHash,
496
+ totalMessagesInserted: BigInt(visibleMessages.length),
497
+ treeInProgress: BigInt(treeInProgress),
498
+ });
499
+ });
500
+
501
+ // Mock the wrapper methods for fetching message events
502
+ mockInbox.getMessageSentEvents.mockImplementation((fromBlock: bigint, toBlock: bigint) =>
503
+ Promise.resolve(this.getMessageSentLogs(fromBlock, toBlock)),
504
+ );
505
+
506
+ mockInbox.getMessageSentEventByHash.mockImplementation((msgHash: string, aroundL1BlockNumber: bigint) =>
507
+ Promise.resolve(this.getMessageSentLogByHash(msgHash, aroundL1BlockNumber) as MessageSentLog),
508
+ );
509
+
510
+ return mockInbox;
511
+ }
512
+
513
+ /** Creates mock PublicClient that reads from this fake state. */
514
+ createMockPublicClient(): MockProxy<ViemPublicClient> {
515
+ const publicClient = mock<ViemPublicClient>();
516
+
517
+ publicClient.getChainId.mockResolvedValue(1);
518
+ // Several consumers (CalldataRetriever, ArchiverL1Synchronizer) derive the EIP-712 signing
519
+ // context from `publicClient.chain.id`. Pin it so it matches `getSignatureContext()` below.
520
+ (publicClient as unknown as { chain: { id: number } }).chain = { id: 1 };
521
+ publicClient.getBlockNumber.mockImplementation(() => Promise.resolve(this.l1BlockNumber));
522
+
523
+ publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint; blockTag?: string } = {}) => {
524
+ let blockNum: bigint;
525
+ if (args.blockTag === 'finalized') {
526
+ if (this.finalizedL1BlockNumber === undefined) {
527
+ throw Object.assign(new Error('finalized block not found'), {
528
+ details: 'finalized block not found',
529
+ });
530
+ }
531
+ blockNum = this.finalizedL1BlockNumber;
532
+ } else {
533
+ blockNum = args.blockNumber ?? (await publicClient.getBlockNumber());
534
+ }
535
+ return {
536
+ number: blockNum,
537
+ timestamp: BigInt(blockNum) * BigInt(this.config.ethereumSlotDuration) + this.config.l1GenesisTime,
538
+ hash: Buffer32.fromBigInt(blockNum).toString(),
539
+ } as FormattedBlock;
540
+ }) as any);
541
+
542
+ // Use the same pattern as existing test for getTransaction
543
+ publicClient.getTransaction.mockImplementation((args: { hash?: `0x${string}` }) =>
544
+ Promise.resolve(
545
+ args.hash ? (this.checkpoints.find(cpData => cpData.tx.hash === args.hash)?.tx as any) : undefined,
546
+ ),
547
+ );
548
+
549
+ return publicClient;
550
+ }
551
+
552
+ /** Creates mock BlobClient that reads from this fake state. */
553
+ createMockBlobClient(): MockProxy<BlobClientInterface> {
554
+ const blobClient = mock<BlobClientInterface>();
555
+
556
+ // The blockId is the L1 block hash, which we derive from the L1 block number
557
+ blobClient.getBlobSidecar.mockImplementation((blockId: `0x${string}`) =>
558
+ Promise.resolve(
559
+ this.checkpoints.find(cpData => Buffer32.fromBigInt(cpData.l1BlockNumber).toString() === blockId)?.blobs ?? [],
560
+ ),
561
+ );
562
+
563
+ blobClient.testSources.mockResolvedValue(undefined);
564
+
565
+ return blobClient;
566
+ }
567
+
568
+ private updatePendingCheckpointNumber(): void {
569
+ this.pendingCheckpointNumber = this.checkpoints
570
+ .filter(cpData => cpData.l1BlockNumber <= this.l1BlockNumber && !cpData.pruned)
571
+ .reduce((max, cpData) => (cpData.checkpointNumber > max ? cpData.checkpointNumber : max), CheckpointNumber(0));
572
+ }
573
+
574
+ private getArchiveAt(checkpointNumber: CheckpointNumber): Fr {
575
+ if (checkpointNumber === 0) {
576
+ return this.config.genesisArchiveRoot;
577
+ }
578
+ // Find the latest non-pruned entry for this checkpoint number
579
+ const entries = this.checkpoints.filter(cpData => cpData.checkpointNumber === checkpointNumber);
580
+ const latestEntry = entries.at(-1);
581
+
582
+ if (!latestEntry || latestEntry.pruned) {
583
+ // For pruned or missing checkpoints, return the previous non-pruned checkpoint's archive
584
+ return this.getArchiveAt(CheckpointNumber(checkpointNumber - 1));
585
+ }
586
+ return latestEntry.checkpoint.archive.root;
587
+ }
588
+
589
+ private getNextBlockNumber(checkpointNumber: CheckpointNumber): BlockNumber {
590
+ const lastBlockNumber = this.checkpoints
591
+ .filter(cpData => cpData.checkpointNumber < checkpointNumber)
592
+ .flatMap(cpData => cpData.checkpoint.blocks.map(b => b.number))
593
+ .reduce((max, num) => Math.max(max, num), 0);
594
+ return (lastBlockNumber + 1) as BlockNumber;
595
+ }
596
+
597
+ private getCheckpointProposedLogs(fromBlock: bigint, toBlock: bigint): CheckpointProposedLog[] {
598
+ return this.checkpoints
599
+ .filter(cpData => cpData.l1BlockNumber >= fromBlock && cpData.l1BlockNumber <= toBlock)
600
+ .map(cpData => ({
601
+ l1BlockNumber: cpData.l1BlockNumber,
602
+ l1BlockHash: Buffer32.fromBigInt(cpData.l1BlockNumber),
603
+ l1TransactionHash: cpData.tx.hash as `0x${string}`,
604
+ args: {
605
+ checkpointNumber: cpData.checkpointNumber,
606
+ archive: cpData.checkpoint.archive.root,
607
+ versionedBlobHashes: cpData.blobHashes.map(h => Buffer.from(h.slice(2), 'hex')),
608
+ attestationsHash: cpData.attestationsHash,
609
+ payloadDigest: cpData.payloadDigest,
610
+ },
611
+ }));
612
+ }
613
+
614
+ private getMessageSentLogs(fromBlock?: bigint, toBlock?: bigint, hashFilter?: string): MessageSentLog[] {
615
+ return this.messages
616
+ .filter(
617
+ msg =>
618
+ (!hashFilter || msg.leaf.toString() === hashFilter) &&
619
+ (!fromBlock || msg.l1BlockNumber >= fromBlock) &&
620
+ (!toBlock || msg.l1BlockNumber <= toBlock),
621
+ )
622
+ .map(msg => ({
623
+ l1BlockNumber: msg.l1BlockNumber,
624
+ l1BlockHash: Buffer32.fromBigInt(msg.l1BlockNumber),
625
+ l1TransactionHash: `0x${msg.l1BlockNumber.toString(16)}` as `0x${string}`,
626
+ args: {
627
+ checkpointNumber: msg.checkpointNumber,
628
+ index: msg.index,
629
+ leaf: msg.leaf,
630
+ rollingHash: msg.rollingHash,
631
+ },
632
+ }));
633
+ }
634
+
635
+ private getMessageSentLogByHash(msgHash: string, aroundL1BlockNumber: bigint): MessageSentLog | undefined {
636
+ const msg = this.messages.find(
637
+ msg =>
638
+ msg.leaf.toString() === msgHash &&
639
+ msg.l1BlockNumber >= aroundL1BlockNumber - 5n &&
640
+ msg.l1BlockNumber <= aroundL1BlockNumber + 5n,
641
+ );
642
+ if (!msg) {
643
+ return undefined;
644
+ }
645
+ return {
646
+ l1BlockNumber: msg.l1BlockNumber,
647
+ l1BlockHash: Buffer32.fromBigInt(msg.l1BlockNumber),
648
+ l1TransactionHash: `0x${msg.l1BlockNumber.toString(16)}` as `0x${string}`,
649
+ args: {
650
+ checkpointNumber: msg.checkpointNumber,
651
+ index: msg.index,
652
+ leaf: msg.leaf,
653
+ rollingHash: msg.rollingHash,
654
+ },
655
+ };
656
+ }
657
+
658
+ private async makeRollupTx(
659
+ checkpoint: Checkpoint,
660
+ signers: Secp256k1Signer[],
661
+ ): Promise<{ tx: Transaction; attestationsHash: Buffer32; payloadDigest: Buffer32 }> {
662
+ const signatureContext = this.getSignatureContext();
663
+ const consensusPayload = ConsensusPayload.fromCheckpoint(checkpoint, signatureContext);
664
+ const attestationDigest = getHashedSignaturePayloadTypedData(consensusPayload);
665
+ const attestations = signers
666
+ .map(signer => CommitteeAttestation.fromSignature(signer.sign(attestationDigest)))
667
+ .map(committeeAttestation => committeeAttestation.toViem());
668
+
669
+ const header = checkpoint.header.toViem();
670
+ const blobInput = getPrefixedEthBlobCommitments(await getBlobsPerL1Block(checkpoint.toBlobFields()));
671
+ const archive = toHex(checkpoint.archive.root.toBuffer());
672
+ const attestationsAndSigners = new CommitteeAttestationsAndSigners(
673
+ attestations.map(attestation => CommitteeAttestation.fromViem(attestation)),
674
+ signatureContext,
675
+ );
676
+
677
+ // Fall back to a random signer when no attesters are provided, so tests that
678
+ // don't care about the proposer identity (e.g. sync tests) still produce a
679
+ // valid-looking signature for the attestationsAndSigners struct.
680
+ const proposerSigner = signers[0] ?? Secp256k1Signer.random();
681
+ const attestationsAndSignersSignature = proposerSigner.sign(
682
+ getHashedSignaturePayloadTypedData(attestationsAndSigners),
683
+ );
684
+
685
+ const packedAttestations = attestationsAndSigners.getPackedAttestations();
686
+
687
+ const rollupInput = encodeFunctionData({
688
+ abi: RollupAbi,
689
+ functionName: 'propose',
690
+ args: [
691
+ {
692
+ header,
693
+ archive,
694
+ oracleInput: { feeAssetPriceModifier: 0n },
695
+ },
696
+ packedAttestations,
697
+ attestationsAndSigners.getSigners().map(signer => signer.toString()),
698
+ attestationsAndSignersSignature.toViemSignature(),
699
+ blobInput,
700
+ ],
701
+ });
702
+
703
+ const multiCallInput = encodeFunctionData({
704
+ abi: multicall3Abi,
705
+ functionName: 'aggregate3',
706
+ args: [
707
+ [
708
+ {
709
+ target: this.config.rollupAddress.toString(),
710
+ callData: rollupInput,
711
+ allowFailure: false,
712
+ },
713
+ ],
714
+ ],
715
+ });
716
+
717
+ // Compute attestationsHash (same logic as CalldataRetriever)
718
+ const attestationsHash = Buffer32.fromString(
719
+ keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations])),
720
+ );
721
+
722
+ // Compute payloadDigest (same logic as CalldataRetriever)
723
+ const payloadDigest = getHashedSignaturePayloadTypedData(consensusPayload);
724
+
725
+ const tx = {
726
+ input: multiCallInput,
727
+ hash: archive,
728
+ blockHash: archive,
729
+ to: MULTI_CALL_3_ADDRESS as `0x${string}`,
730
+ } as Transaction<bigint, number>;
731
+
732
+ return { tx, attestationsHash, payloadDigest };
733
+ }
734
+
735
+ private getSignatureContext() {
736
+ return {
737
+ chainId: 1,
738
+ rollupAddress: this.config.rollupAddress,
739
+ };
740
+ }
741
+
742
+ /** Extracts the CommitteeAttestations struct definition from RollupAbi for hash computation. */
743
+ private getCommitteeAttestationsStructDef(): AbiParameter {
744
+ const proposeFunction = RollupAbi.find(item => item.type === 'function' && item.name === 'propose') as
745
+ | { type: 'function'; name: string; inputs: readonly AbiParameter[] }
746
+ | undefined;
747
+
748
+ if (!proposeFunction) {
749
+ throw new Error('propose function not found in RollupAbi');
750
+ }
751
+
752
+ const attestationsParam = proposeFunction.inputs.find(param => param.name === '_attestations');
753
+ if (!attestationsParam) {
754
+ throw new Error('_attestations parameter not found in propose function');
755
+ }
756
+
757
+ const tupleParam = attestationsParam as unknown as { type: 'tuple'; components?: readonly AbiParameter[] };
758
+ return { type: 'tuple', components: tupleParam.components || [] } as AbiParameter;
759
+ }
760
+
761
+ private async makeVersionedBlobHashes(checkpoint: Checkpoint): Promise<`0x${string}`[]> {
762
+ return (await getBlobsPerL1Block(checkpoint.toBlobFields())).map(
763
+ b => `0x${b.getEthVersionedBlobHash().toString('hex')}` as `0x${string}`,
764
+ );
765
+ }
766
+
767
+ private async makeBlobsFromCheckpoint(checkpoint: Checkpoint): Promise<Blob[]> {
768
+ return await getBlobsPerL1Block(checkpoint.toBlobFields());
769
+ }
770
+ }