anvil_zksync_l1_sidecar/
commitment_generator.rs

1use crate::zkstack_config::ZkstackConfig;
2use anvil_zksync_core::node::blockchain::ReadBlockchain;
3use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5use zksync_contracts::BaseSystemContractsHashes;
6use zksync_types::blob::num_blobs_required;
7use zksync_types::block::{L1BatchHeader, L1BatchTreeData};
8use zksync_types::commitment::{
9    AuxCommitments, CommitmentCommonInput, CommitmentInput, L1BatchCommitment, L1BatchMetadata,
10    L1BatchWithMetadata,
11};
12use zksync_types::writes::StateDiffRecord;
13use zksync_types::L1BatchNumber;
14use zksync_types::{Address, H256};
15
16/// Node component that can generate batch's metadata (with commitment) on demand.
17#[derive(Debug, Clone)]
18pub struct CommitmentGenerator {
19    /// System contracts hashes expected by L1. Might be different from the actual contract hashes used by anvil-zksync.
20    base_system_contracts_hashes: BaseSystemContractsHashes,
21    /// Fee address expected by L1.
22    fee_address: Address,
23    blockchain: Box<dyn ReadBlockchain>,
24    /// Batches with already known metadata.
25    batches: Arc<RwLock<HashMap<L1BatchNumber, L1BatchWithMetadata>>>,
26}
27
28impl CommitmentGenerator {
29    /// Initializes a new [`CommitmentGenerator`] matching L1 from the provided zkstack config.
30    ///
31    /// Additionally, returns genesis' metadata.
32    pub fn new(zkstack_config: &ZkstackConfig, blockchain: Box<dyn ReadBlockchain>) -> Self {
33        // L1 expects a specific hash for bootloader/AA contracts which might be different from what
34        // anvil-zksync is actually using (e.g. it is running with a user-provided bootloader). Thus,
35        // we use hash from zkstack genesis config, i.e. what L1 was initialized with.
36        let base_system_contracts_hashes = BaseSystemContractsHashes {
37            bootloader: zkstack_config.genesis.bootloader_hash,
38            default_aa: zkstack_config.genesis.default_aa_hash,
39            evm_emulator: zkstack_config.genesis.evm_emulator_hash,
40        };
41
42        // Run realistic genesis commitment computation that should match value from zkstack config
43        let mut genesis_batch_header = L1BatchHeader::new(
44            L1BatchNumber(0),
45            0,
46            base_system_contracts_hashes,
47            zkstack_config.genesis.genesis_protocol_version,
48        );
49        genesis_batch_header.fee_address = zkstack_config.genesis.fee_account;
50        let commitment_input = CommitmentInput::for_genesis_batch(
51            zkstack_config.genesis.genesis_root,
52            zkstack_config.genesis.genesis_rollup_leaf_index,
53            base_system_contracts_hashes,
54            zkstack_config.genesis.genesis_protocol_version,
55        );
56        let genesis_metadata =
57            Self::generate_metadata_inner(genesis_batch_header, commitment_input);
58        assert_eq!(
59            zkstack_config.genesis.genesis_batch_commitment, genesis_metadata.metadata.commitment,
60            "Computed genesis batch commitment does not match zkstack config"
61        );
62
63        Self {
64            base_system_contracts_hashes,
65            fee_address: zkstack_config.genesis.fee_account,
66            blockchain,
67            batches: Arc::new(RwLock::new(HashMap::from_iter([(
68                L1BatchNumber(0),
69                genesis_metadata,
70            )]))),
71        }
72    }
73
74    /// Retrieve batch's existing metadata or generate it if there is none. Returns `None` if batch
75    /// with this number does not exist.
76    pub async fn get_or_generate_metadata(
77        &self,
78        batch_number: L1BatchNumber,
79    ) -> Option<L1BatchWithMetadata> {
80        if let Some(metadata) = self.batches.read().unwrap().get(&batch_number) {
81            return Some(metadata.clone());
82        }
83
84        // Fetch batch header from storage and patch its fee_address/base_system_contract_hashes as
85        // those might be different from what L1 expects (e.g. impersonated execution, custom
86        // user-supplied contracts etc).
87        let mut header = self.blockchain.get_batch_header(batch_number).await?;
88        header.fee_address = self.fee_address;
89        header.base_system_contracts_hashes = self.base_system_contracts_hashes;
90
91        let state_diffs = self.blockchain.get_batch_state_diffs(batch_number).await?;
92        let aggregation_root = self
93            .blockchain
94            .get_batch_aggregation_root(batch_number)
95            .await?;
96
97        // anvil-zksync does not store batches right now so we just generate dummy commitment input.
98        // Root hash is random purely so that different batches have different commitments.
99        let tree_data = L1BatchTreeData {
100            hash: H256::random(),
101            rollup_last_leaf_index: 42,
102        };
103        let metadata = self.generate_metadata(header, state_diffs, aggregation_root, tree_data);
104        self.batches
105            .write()
106            .unwrap()
107            .insert(batch_number, metadata.clone());
108        Some(metadata)
109    }
110
111    fn generate_metadata(
112        &self,
113        header: L1BatchHeader,
114        state_diffs: Vec<StateDiffRecord>,
115        aggregation_root: H256,
116        tree_data: L1BatchTreeData,
117    ) -> L1BatchWithMetadata {
118        let protocol_version = header
119            .protocol_version
120            .expect("batch header is missing protocol version");
121        let common = CommitmentCommonInput {
122            l2_to_l1_logs: header.l2_to_l1_logs.clone(),
123            rollup_last_leaf_index: tree_data.rollup_last_leaf_index,
124            rollup_root_hash: tree_data.hash,
125            bootloader_code_hash: self.base_system_contracts_hashes.bootloader,
126            default_aa_code_hash: self.base_system_contracts_hashes.default_aa,
127            evm_emulator_code_hash: self.base_system_contracts_hashes.evm_emulator,
128            protocol_version,
129        };
130        let commitment_input = CommitmentInput::PostBoojum {
131            common,
132            system_logs: header.system_logs.clone(),
133            state_diffs,
134            aux_commitments: AuxCommitments {
135                events_queue_commitment: H256::zero(),
136                bootloader_initial_content_commitment: H256::zero(),
137            },
138            blob_hashes: {
139                let num_blobs = num_blobs_required(&protocol_version);
140                vec![Default::default(); num_blobs]
141            },
142            aggregation_root,
143        };
144
145        Self::generate_metadata_inner(header, commitment_input)
146    }
147
148    fn generate_metadata_inner(
149        header: L1BatchHeader,
150        commitment_input: CommitmentInput,
151    ) -> L1BatchWithMetadata {
152        let root_hash = commitment_input.common().rollup_root_hash;
153        let rollup_last_leaf_index = commitment_input.common().rollup_last_leaf_index;
154
155        let commitment = L1BatchCommitment::new(commitment_input);
156        let mut commitment_artifacts = commitment.artifacts();
157        if header.number == L1BatchNumber(0) {
158            // `l2_l1_merkle_root` for genesis batch is set to 0 on L1 contract, same must be here.
159            commitment_artifacts.l2_l1_merkle_root = H256::zero();
160        }
161        tracing::debug!(
162            batch = header.number.0,
163            commitment_hash = ?commitment_artifacts.commitment_hash.commitment,
164            "generated a new batch commitment",
165        );
166        // Unlike above, this is a realistic metadata calculation based on code from zksync-era.
167        let batch_metadata = L1BatchMetadata {
168            root_hash,
169            rollup_last_leaf_index,
170            initial_writes_compressed: commitment_artifacts.compressed_initial_writes,
171            repeated_writes_compressed: commitment_artifacts.compressed_repeated_writes,
172            commitment: commitment_artifacts.commitment_hash.commitment,
173            l2_l1_merkle_root: commitment_artifacts.l2_l1_merkle_root,
174            block_meta_params: commitment.meta_parameters(),
175            aux_data_hash: commitment_artifacts.commitment_hash.aux_output,
176            meta_parameters_hash: commitment_artifacts.commitment_hash.meta_parameters,
177            pass_through_data_hash: commitment_artifacts.commitment_hash.pass_through_data,
178            events_queue_commitment: commitment_artifacts
179                .aux_commitments
180                .map(|a| a.events_queue_commitment),
181            bootloader_initial_content_commitment: commitment_artifacts
182                .aux_commitments
183                .map(|a| a.bootloader_initial_content_commitment),
184            state_diffs_compressed: commitment_artifacts
185                .compressed_state_diffs
186                .unwrap_or_default(),
187            state_diff_hash: Some(commitment_artifacts.state_diff_hash),
188            local_root: Some(commitment_artifacts.local_root),
189            aggregation_root: Some(commitment_artifacts.aggregation_root),
190            // anvil-zksync can only be run in rollup mode which does not have DA inclusion
191            da_inclusion_data: None,
192        };
193
194        // Pretend there were no used factory deps and no published bytecode.
195        L1BatchWithMetadata::new(header, batch_metadata, HashMap::new(), &[])
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use anvil_zksync_core::filters::LogFilter;
203    use async_trait::async_trait;
204    use zksync_types::api::{
205        Block, BlockDetails, BlockId, DebugCall, Log, Transaction, TransactionDetails,
206        TransactionReceipt, TransactionVariant,
207    };
208    use zksync_types::web3::Bytes;
209    use zksync_types::{L2BlockNumber, ProtocolVersionId};
210
211    // TODO: Consider moving to a separate testing crate
212    #[derive(Clone, Debug)]
213    struct MockBlockchain(HashMap<L1BatchNumber, L1BatchHeader>);
214
215    impl MockBlockchain {
216        pub fn new(batches: impl IntoIterator<Item = L1BatchHeader>) -> Self {
217            let genesis = L1BatchHeader::new(
218                L1BatchNumber(0),
219                0,
220                BaseSystemContractsHashes::default(),
221                ProtocolVersionId::latest(),
222            );
223            Self(HashMap::from_iter(
224                batches
225                    .into_iter()
226                    .map(|h| (h.number, h))
227                    .chain([(L1BatchNumber(0), genesis)]),
228            ))
229        }
230    }
231
232    #[async_trait]
233    impl ReadBlockchain for MockBlockchain {
234        fn dyn_cloned(&self) -> Box<dyn ReadBlockchain> {
235            unimplemented!()
236        }
237
238        fn protocol_version(&self) -> ProtocolVersionId {
239            unimplemented!()
240        }
241
242        async fn current_batch(&self) -> L1BatchNumber {
243            unimplemented!()
244        }
245
246        async fn current_block_number(&self) -> L2BlockNumber {
247            unimplemented!()
248        }
249
250        async fn current_block_hash(&self) -> H256 {
251            unimplemented!()
252        }
253
254        async fn get_block_by_hash(&self, _hash: &H256) -> Option<Block<TransactionVariant>> {
255            unimplemented!()
256        }
257
258        async fn get_block_by_number(
259            &self,
260            _number: L2BlockNumber,
261        ) -> Option<Block<TransactionVariant>> {
262            unimplemented!()
263        }
264
265        async fn get_block_by_id(&self, _block_id: BlockId) -> Option<Block<TransactionVariant>> {
266            unimplemented!()
267        }
268
269        async fn get_block_hash_by_number(&self, _number: L2BlockNumber) -> Option<H256> {
270            unimplemented!()
271        }
272
273        async fn get_block_hash_by_id(&self, _block_id: BlockId) -> Option<H256> {
274            unimplemented!()
275        }
276
277        async fn get_block_number_by_hash(&self, _hash: &H256) -> Option<L2BlockNumber> {
278            unimplemented!()
279        }
280
281        async fn get_block_number_by_id(&self, _block_id: BlockId) -> Option<L2BlockNumber> {
282            unimplemented!()
283        }
284
285        async fn get_block_tx_hashes_by_number(&self, _number: L2BlockNumber) -> Option<Vec<H256>> {
286            unimplemented!()
287        }
288
289        async fn get_block_tx_hashes_by_id(&self, _block_id: BlockId) -> Option<Vec<H256>> {
290            unimplemented!()
291        }
292
293        async fn get_block_tx_by_id(
294            &self,
295            _block_id: BlockId,
296            _index: usize,
297        ) -> Option<Transaction> {
298            unimplemented!()
299        }
300
301        async fn get_block_tx_count_by_id(&self, _block_id: BlockId) -> Option<usize> {
302            unimplemented!()
303        }
304
305        async fn get_block_details_by_number(
306            &self,
307            _number: L2BlockNumber,
308            _l2_fair_gas_price: u64,
309            _fair_pubdata_price: Option<u64>,
310            _base_system_contracts_hashes: BaseSystemContractsHashes,
311        ) -> Option<BlockDetails> {
312            unimplemented!()
313        }
314
315        async fn get_tx_receipt(&self, _tx_hash: &H256) -> Option<TransactionReceipt> {
316            unimplemented!()
317        }
318
319        async fn get_tx_debug_info(&self, _tx_hash: &H256, _only_top: bool) -> Option<DebugCall> {
320            unimplemented!()
321        }
322
323        async fn get_tx_api(&self, _tx_hash: &H256) -> anyhow::Result<Option<Transaction>> {
324            unimplemented!()
325        }
326
327        async fn get_detailed_tx(
328            &self,
329            _tx: Transaction,
330        ) -> Option<anvil_zksync_types::api::DetailedTransaction> {
331            unimplemented!()
332        }
333
334        async fn get_tx_details(&self, _tx_hash: &H256) -> Option<TransactionDetails> {
335            unimplemented!()
336        }
337
338        async fn get_zksync_tx(&self, _tx_hash: &H256) -> Option<zksync_types::Transaction> {
339            unimplemented!()
340        }
341
342        async fn get_filter_logs(&self, _log_filter: &LogFilter) -> Vec<Log> {
343            unimplemented!()
344        }
345
346        async fn get_batch_header(&self, batch_number: L1BatchNumber) -> Option<L1BatchHeader> {
347            self.0.get(&batch_number).cloned()
348        }
349
350        async fn get_batch_state_diffs(
351            &self,
352            batch_number: L1BatchNumber,
353        ) -> Option<Vec<StateDiffRecord>> {
354            if self.0.contains_key(&batch_number) {
355                Some(vec![])
356            } else {
357                None
358            }
359        }
360
361        async fn get_batch_aggregation_root(&self, batch_number: L1BatchNumber) -> Option<H256> {
362            if self.0.contains_key(&batch_number) {
363                Some(H256::zero())
364            } else {
365                None
366            }
367        }
368
369        async fn get_raw_transaction(&self, _tx_hash: H256) -> Option<Bytes> {
370            unimplemented!()
371        }
372        async fn get_raw_transactions(&self, _block_number: BlockId) -> Vec<Bytes> {
373            unimplemented!()
374        }
375    }
376
377    #[tokio::test]
378    async fn generates_proper_genesis() {
379        let config = ZkstackConfig::builtin(ProtocolVersionId::latest());
380        let blockchain = MockBlockchain::new([]);
381        let commitment_generator = CommitmentGenerator::new(&config, Box::new(blockchain));
382        let genesis_metadata = commitment_generator
383            .get_or_generate_metadata(L1BatchNumber(0))
384            .await
385            .unwrap();
386        // Basic invariants expected by the protocol
387        assert_eq!(genesis_metadata.header.number, L1BatchNumber(0));
388        assert_eq!(genesis_metadata.header.timestamp, 0);
389        assert_eq!(genesis_metadata.header.l1_tx_count, 0);
390        assert_eq!(genesis_metadata.header.l2_tx_count, 0);
391
392        // Computed genesis should match provided config
393        assert_eq!(
394            genesis_metadata.metadata.root_hash,
395            config.genesis.genesis_root
396        );
397        assert_eq!(
398            genesis_metadata.metadata.rollup_last_leaf_index,
399            config.genesis.genesis_rollup_leaf_index
400        );
401        assert_eq!(
402            genesis_metadata.metadata.commitment,
403            config.genesis.genesis_batch_commitment
404        );
405    }
406
407    #[tokio::test]
408    async fn returns_none_for_unknown_batch() {
409        let config = ZkstackConfig::builtin(ProtocolVersionId::latest());
410        let blockchain = MockBlockchain::new([]);
411        let commitment_generator = CommitmentGenerator::new(&config, Box::new(blockchain));
412        let metadata = commitment_generator
413            .get_or_generate_metadata(L1BatchNumber(42))
414            .await;
415
416        assert_eq!(metadata, None);
417    }
418
419    #[tokio::test]
420    async fn generates_valid_commitment_for_random_batch() {
421        let config = ZkstackConfig::builtin(ProtocolVersionId::latest());
422        let batch_42_header = L1BatchHeader::new(
423            L1BatchNumber(42),
424            1042,
425            BaseSystemContractsHashes::default(),
426            ProtocolVersionId::latest(),
427        );
428        let blockchain = MockBlockchain::new([batch_42_header.clone()]);
429        let commitment_generator = CommitmentGenerator::new(&config, Box::new(blockchain));
430        let metadata = commitment_generator
431            .get_or_generate_metadata(L1BatchNumber(42))
432            .await
433            .unwrap();
434
435        // Really this is all we can check without making assumptions about the implementation
436        assert_eq!(metadata.header.number, batch_42_header.number);
437        assert_eq!(metadata.header.timestamp, batch_42_header.timestamp);
438    }
439}