clementine_core/
header_chain_prover.rs

1//! # Header Chain Prover
2//!
3//! This module contains utilities for proving Bitcoin block headers. This
4//! module must be fed with new blocks via the database. Later, it can check if
5//! proving should be triggered by verifying if the batch size is sufficient.
6
7use crate::builder::block_cache::BlockCache;
8use crate::database::DatabaseTransaction;
9use crate::{config::BridgeConfig, database::Database, extended_bitcoin_rpc::ExtendedBitcoinRpc};
10use bitcoin::block::Header;
11use bitcoin::{hashes::Hash, BlockHash, Network};
12use bitcoincore_rpc::RpcApi;
13use bridge_circuit_host::bridge_circuit_host::{
14    MAINNET_HEADER_CHAIN_ELF, MAINNET_WORK_ONLY_ELF, REGTEST_HEADER_CHAIN_ELF,
15    REGTEST_WORK_ONLY_ELF, SIGNET_HEADER_CHAIN_ELF, SIGNET_WORK_ONLY_ELF,
16    TESTNET4_HEADER_CHAIN_ELF, TESTNET4_WORK_ONLY_ELF,
17};
18use bridge_circuit_host::docker::dev_stark_to_risc0_g16;
19use bridge_circuit_host::utils::is_dev_mode;
20use circuits_lib::bridge_circuit::structs::{WorkOnlyCircuitInput, WorkOnlyCircuitOutput};
21use circuits_lib::header_chain::mmr_guest::MMRGuest;
22use circuits_lib::header_chain::{
23    BlockHeaderCircuitOutput, ChainState, CircuitBlockHeader, HeaderChainCircuitInput,
24    HeaderChainPrevProofType,
25};
26use clementine_errors::{BridgeError, ErrorExt, ResultExt};
27use eyre::{eyre, Context, OptionExt};
28use lazy_static::lazy_static;
29use risc0_zkvm::{compute_image_id, ExecutorEnv, ProverOpts, Receipt};
30use std::{
31    fs::File,
32    io::{BufReader, Read},
33};
34
35lazy_static! {
36    static ref MAINNET_HCP_METHOD_ID: [u32; 8] = compute_image_id(MAINNET_HEADER_CHAIN_ELF)
37        .expect("hardcoded ELF is valid")
38        .as_words()
39        .try_into()
40        .expect("hardcoded ELF is valid");
41    static ref TESTNET4_HCP_METHOD_ID: [u32; 8] = compute_image_id(TESTNET4_HEADER_CHAIN_ELF)
42        .expect("hardcoded ELF is valid")
43        .as_words()
44        .try_into()
45        .expect("hardcoded ELF is valid");
46    static ref SIGNET_HCP_METHOD_ID: [u32; 8] = compute_image_id(SIGNET_HEADER_CHAIN_ELF)
47        .expect("hardcoded ELF is valid")
48        .as_words()
49        .try_into()
50        .expect("hardcoded ELF is valid");
51    static ref REGTEST_HCP_METHOD_ID: [u32; 8] = compute_image_id(REGTEST_HEADER_CHAIN_ELF)
52        .expect("hardcoded ELF is valid")
53        .as_words()
54        .try_into()
55        .expect("hardcoded ELF is valid");
56}
57
58pub use clementine_errors::HeaderChainProverError;
59
60#[derive(Debug, Clone)]
61pub struct HeaderChainProver {
62    db: Database,
63    network: bitcoin::Network,
64    batch_size: u64,
65}
66
67impl HeaderChainProver {
68    /// Creates a new [`HeaderChainProver`] instance. Also saves a proof
69    /// assumption if specified in the config.
70    pub async fn new(
71        config: &BridgeConfig,
72        rpc: ExtendedBitcoinRpc,
73    ) -> Result<Self, HeaderChainProverError> {
74        let db = Database::new(config).await.map_to_eyre()?;
75        let tip_height = rpc.get_current_chain_height().await.map_to_eyre()?;
76        if !config
77            .protocol_paramset()
78            .is_block_finalized(config.protocol_paramset().start_height, tip_height)
79        {
80            return Err(eyre::eyre!(
81                "Start height is not finalized, reduce start height: {} < {}",
82                tip_height,
83                config.protocol_paramset().start_height + config.protocol_paramset().finality_depth
84                    - 1
85            )
86            .into());
87        }
88        db.fetch_and_save_missing_blocks(
89            None,
90            &rpc,
91            config.protocol_paramset().genesis_height,
92            config.protocol_paramset().start_height,
93        )
94        .await
95        .wrap_err("Failed to save initial block infos")?;
96
97        if let Some(proof_file) = &config.header_chain_proof_path {
98            tracing::info!("Starting prover with assumption file {:?}.", proof_file);
99            let file = File::open(proof_file)
100                .wrap_err_with(|| format!("Failed to open proof assumption file {proof_file:?}"))?;
101
102            let mut reader = BufReader::new(file);
103            let mut assumption = Vec::new();
104            reader
105                .read_to_end(&mut assumption)
106                .wrap_err("Can't read assumption file")?;
107
108            let proof: Receipt = borsh::from_slice(&assumption)
109                .wrap_err(HeaderChainProverError::ProverDeSerializationError)?;
110            let proof_output: BlockHeaderCircuitOutput = borsh::from_slice(&proof.journal.bytes)
111                .wrap_err(HeaderChainProverError::ProverDeSerializationError)?;
112
113            let network = config.protocol_paramset().network;
114
115            if proof_output.method_id != get_hcp_method_id(network)? {
116                return Err(eyre::eyre!(
117                    "Header chain proof assumption file Method ID mismatch for our current network ({:?}): got {:?}, expected {:?}",
118                    network,
119                    proof_output.method_id,
120                    get_hcp_method_id(network)?
121                )
122                .into());
123            }
124
125            if proof.verify(proof_output.method_id).is_err() {
126                return Err(eyre::eyre!(
127                    "Header chain proof assumption file verification failed for our current network ({:?})",
128                    network
129                )
130                .into());
131            }
132
133            // Create block entry, if not exists.
134            let block_hash = BlockHash::from_raw_hash(
135                Hash::from_slice(&proof_output.chain_state.best_block_hash).map_to_eyre()?,
136            );
137            let block_header = rpc.get_block_header(&block_hash).await.wrap_err(format!(
138                "Failed to get block header with block hash {block_hash} (retrieved from assumption file)",
139            ))?;
140            let block_height = rpc
141                .get_block_info(&block_hash)
142                .await
143                .map(|info| info.height)
144                .wrap_err(format!(
145                    "Failed to get block info with block hash {block_hash} (retrieved from assumption file)"
146                ))?;
147            tracing::info!(
148                "Adding proof assumption for a block with hash of {:?}, header of {:?} and height of {}",
149                block_hash,
150                block_header,
151                block_height
152            );
153
154            // If an unproven block in database already exists, it shouldn't
155            // effect anything.
156            // PS: This also ignores other db errors but there are other places
157            // where we check for those errors.
158            let _ = db
159                .save_unproven_finalized_block(
160                    None,
161                    block_hash,
162                    block_header,
163                    proof_output.chain_state.block_height.into(),
164                )
165                .await
166                .inspect_err(|e| {
167                    tracing::debug!("Can't set initial block info for header chain prover, because: {e}. Doesn't affect anything, continuing...");
168                });
169
170            db.set_block_proof(None, block_hash, proof)
171                .await
172                .map_to_eyre()?;
173        } else {
174            tracing::info!("Starting prover without assumption, proving genesis block");
175
176            let genesis_block_hash = rpc
177                .get_block_hash(config.protocol_paramset().genesis_height.into())
178                .await
179                .wrap_err(format!(
180                    "Failed to get genesis block hash at height {}",
181                    config.protocol_paramset().genesis_height
182                ))?;
183
184            tracing::debug!(
185                "Genesis block hash: {}, height: {}",
186                genesis_block_hash,
187                config.protocol_paramset().genesis_height
188            ); // Should be debug
189
190            let genesis_block_header =
191                rpc.get_block_header(&genesis_block_hash)
192                    .await
193                    .wrap_err(format!(
194                        "Failed to get genesis block header at height {}",
195                        config.protocol_paramset().genesis_height
196                    ))?;
197
198            let genesis_chain_state = HeaderChainProver::get_chain_state_from_height(
199                &rpc,
200                config.protocol_paramset().genesis_height.into(),
201                config.protocol_paramset().network,
202            )
203            .await
204            .map_to_eyre()?;
205            tracing::debug!("Genesis chain state (verbose): {:?}", genesis_chain_state);
206
207            let proof = HeaderChainProver::prove_genesis_block(
208                genesis_chain_state,
209                config.protocol_paramset().network,
210            )
211            .map_to_eyre()?;
212
213            let _ = db
214                .save_unproven_finalized_block(
215                    None,
216                    genesis_block_hash,
217                    genesis_block_header,
218                    config.protocol_paramset().genesis_height.into(),
219                )
220                .await;
221
222            db.set_block_proof(None, genesis_block_hash, proof)
223                .await
224                .map_to_eyre()?;
225        }
226
227        Ok(HeaderChainProver {
228            db,
229            batch_size: config.header_chain_proof_batch_size.into(),
230            network: config.protocol_paramset().network,
231        })
232    }
233
234    pub async fn get_chain_state_from_height(
235        rpc: &ExtendedBitcoinRpc,
236        height: u64,
237        network: Network,
238    ) -> Result<ChainState, HeaderChainProverError> {
239        let block_hash = rpc
240            .get_block_hash(height)
241            .await
242            .wrap_err(format!("Failed to get block hash at height {height}"))?;
243
244        let block_header = rpc.get_block_header(&block_hash).await.wrap_err(format!(
245            "Failed to get block header with block hash {block_hash}"
246        ))?;
247
248        let mut last_11_block_timestamps: [u32; 11] = [0; 11];
249        let mut last_block_hash = block_hash;
250        let mut last_block_height = height;
251        for _ in 0..11 {
252            let block_header = rpc
253                .get_block_header(&last_block_hash)
254                .await
255                .wrap_err(format!(
256                    "Failed to get block header with block hash {last_block_hash}"
257                ))?;
258
259            last_11_block_timestamps[last_block_height as usize % 11] = block_header.time;
260
261            last_block_hash = block_header.prev_blockhash;
262            last_block_height = last_block_height.wrapping_sub(1);
263
264            if last_block_hash.to_byte_array() == [0u8; 32] {
265                break;
266            }
267        }
268
269        let epoch_start_block_height = height / 2016 * 2016;
270
271        let (epoch_start_timestamp, expected_bits) = if network == Network::Regtest {
272            (0, block_header.bits.to_consensus())
273        } else {
274            let epoch_start_block_hash = rpc
275                .get_block_hash(epoch_start_block_height)
276                .await
277                .wrap_err(format!(
278                    "Failed to get block hash at height {epoch_start_block_height}"
279                ))?;
280            let epoch_start_block_header = rpc
281                .get_block_header(&epoch_start_block_hash)
282                .await
283                .wrap_err(format!(
284                    "Failed to get block header with block hash {epoch_start_block_hash}"
285                ))?;
286            let bits = if network == Network::Testnet4 {
287                // Real difficulty will show up at epoch start block no matter what
288                epoch_start_block_header.bits.to_consensus()
289            } else {
290                block_header.bits.to_consensus()
291            };
292
293            (epoch_start_block_header.time, bits)
294        };
295
296        let block_info = rpc.get_block_info(&block_hash).await.wrap_err(format!(
297            "Failed to get block info with block hash {block_hash}"
298        ))?;
299
300        let total_work = block_info.chainwork;
301
302        let total_work: [u8; 32] = total_work.try_into().expect("Total work is 32 bytes");
303
304        let mut block_hashes_mmr = MMRGuest::new();
305        block_hashes_mmr.append(block_hash.to_byte_array());
306
307        let chain_state = ChainState {
308            block_height: height as u32,
309            total_work,
310            best_block_hash: block_hash.to_byte_array(),
311            current_target_bits: expected_bits,
312            epoch_start_time: epoch_start_timestamp,
313            prev_11_timestamps: last_11_block_timestamps,
314            block_hashes_mmr,
315        };
316        Ok(chain_state)
317    }
318
319    /// Proves the work only proof for the given HCP receipt.
320    pub async fn prove_work_only(
321        &self,
322        hcp_receipt: Receipt,
323    ) -> Result<(Receipt, WorkOnlyCircuitOutput), HeaderChainProverError> {
324        let block_header_circuit_output: BlockHeaderCircuitOutput =
325            borsh::from_slice(&hcp_receipt.journal.bytes)
326                .wrap_err(HeaderChainProverError::ProverDeSerializationError)?;
327        let input = WorkOnlyCircuitInput {
328            header_chain_circuit_output: block_header_circuit_output,
329        };
330        let input_bytes = borsh::to_vec(&input).wrap_err(BridgeError::BorshError)?;
331        let network = self.network;
332
333        let elf = match network {
334            Network::Bitcoin => MAINNET_WORK_ONLY_ELF,
335            Network::Testnet4 => TESTNET4_WORK_ONLY_ELF,
336            Network::Signet => SIGNET_WORK_ONLY_ELF,
337            Network::Regtest => REGTEST_WORK_ONLY_ELF,
338            _ => Err(BridgeError::UnsupportedNetwork.into_eyre())?,
339        };
340
341        tracing::warn!("Starting proving HCP work only proof for creating a watchtower challenge");
342        let receipt = tokio::task::spawn_blocking(move || -> Result<Receipt, eyre::Report> {
343            let mut env = ExecutorEnv::builder();
344            env.write_slice(&input_bytes);
345            env.add_assumption(hcp_receipt);
346            let env = env
347                .build()
348                .map_err(|e| eyre::eyre!(e))
349                .wrap_err("Failed to build environment")?;
350
351            let prover = risc0_zkvm::default_prover();
352
353            if !is_dev_mode() {
354                prover
355                    .prove_with_opts(env, elf, &ProverOpts::groth16())
356                    .map_err(|e| eyre::eyre!(e))
357                    .map(|result| result.receipt)
358            } else {
359                let stark_receipt = prover
360                    .prove_with_opts(env, elf, &ProverOpts::succinct())
361                    .map_err(|e| eyre::eyre!(e))?
362                    .receipt;
363                let journal = stark_receipt.journal.bytes.clone();
364                dev_stark_to_risc0_g16(stark_receipt, &journal)
365            }
366        })
367        .await
368        .map_err(|e| eyre::eyre!("Failed to join the prove_work_only task: {}", e))?
369        .wrap_err("Failed to prove work only")?;
370        tracing::warn!("HCP work only proof generated for creating a watchtower challenge");
371        let work_output: WorkOnlyCircuitOutput = borsh::from_slice(&receipt.journal.bytes)
372            .wrap_err(HeaderChainProverError::ProverDeSerializationError)?;
373
374        Ok((receipt, work_output))
375    }
376
377    /// Proves blocks till the block with hash `current_block_hash`.
378    ///
379    /// # Parameters
380    ///
381    /// - `current_block_hash`: Hash of the target block
382    /// - `block_headers`: Previous block headers before the target block
383    /// - `previous_proof`: Previous proof's receipt
384    #[tracing::instrument(skip_all)]
385    async fn prove_and_save_block(
386        &self,
387        current_block_hash: BlockHash,
388        block_headers: Vec<Header>,
389        previous_proof: Receipt,
390    ) -> Result<Receipt, BridgeError> {
391        tracing::debug!(
392            "Prover starts proving {} blocks ending with block with hash {}",
393            block_headers.len(),
394            current_block_hash
395        );
396
397        let headers: Vec<CircuitBlockHeader> = block_headers.into_iter().map(Into::into).collect();
398        let network = self.network;
399        let receipt = tokio::task::spawn_blocking(move || {
400            Self::prove_block_headers(network, previous_proof, headers)
401        })
402        .await
403        .wrap_err("Failed to join the prove_block_headers task")?
404        .wrap_err("Failed to prove block headers")?;
405
406        self.db
407            .set_block_proof(None, current_block_hash, receipt.clone())
408            .await?;
409
410        Ok(receipt)
411    }
412
413    /// Proves given block headers.
414    ///
415    /// # Parameters
416    ///
417    /// - `prev_receipt`: Previous proof's receipt, if not genesis block
418    /// - `block_headers`: Block headers to prove
419    ///
420    /// # Returns
421    ///
422    /// - [`Receipt`]: Proved block headers' proof receipt.
423    fn prove_block_headers(
424        network: Network,
425        prev_receipt: Receipt,
426        block_headers: Vec<CircuitBlockHeader>,
427    ) -> Result<Receipt, HeaderChainProverError> {
428        // Prepare proof input.
429        let prev_output: BlockHeaderCircuitOutput = borsh::from_slice(&prev_receipt.journal.bytes)
430            .wrap_err(HeaderChainProverError::ProverDeSerializationError)?;
431        let method_id = prev_output.method_id;
432
433        let prev_proof = HeaderChainPrevProofType::PrevProof(prev_output);
434
435        let input = HeaderChainCircuitInput {
436            method_id,
437            prev_proof,
438            block_headers,
439        };
440        Self::prove_with_input(input, Some(prev_receipt), network)
441    }
442
443    pub fn prove_genesis_block(
444        genesis_chain_state: ChainState,
445        network: Network,
446    ) -> Result<Receipt, HeaderChainProverError> {
447        let image_id = get_hcp_method_id(network)?;
448        let header_chain_circuit_type = HeaderChainPrevProofType::GenesisBlock(genesis_chain_state);
449        let input = HeaderChainCircuitInput {
450            method_id: image_id,
451            prev_proof: header_chain_circuit_type,
452            block_headers: vec![],
453        };
454
455        Self::prove_with_input(input, None, network)
456    }
457
458    fn prove_with_input(
459        input: HeaderChainCircuitInput,
460        prev_receipt: Option<Receipt>,
461        network: Network,
462    ) -> Result<Receipt, HeaderChainProverError> {
463        let mut env = ExecutorEnv::builder();
464
465        env.write_slice(&borsh::to_vec(&input).wrap_err(BridgeError::BorshError)?);
466
467        if let Some(prev_receipt) = prev_receipt {
468            env.add_assumption(prev_receipt);
469        }
470
471        let env = env
472            .build()
473            .map_err(|e| eyre::eyre!(e))
474            .wrap_err("Failed to build environment")?;
475
476        let prover = risc0_zkvm::default_prover();
477
478        let elf = match network {
479            Network::Bitcoin => MAINNET_HEADER_CHAIN_ELF,
480            Network::Testnet4 => TESTNET4_HEADER_CHAIN_ELF,
481            Network::Signet => SIGNET_HEADER_CHAIN_ELF,
482            Network::Regtest => REGTEST_HEADER_CHAIN_ELF,
483            _ => Err(BridgeError::UnsupportedNetwork.into_eyre())?,
484        };
485
486        let receipt = prover.prove(env, elf).map_err(|e| eyre::eyre!(e))?.receipt;
487        tracing::debug!(
488            "Proof receipt for header chain circuit input {:?}: {:?}",
489            input,
490            receipt
491        );
492
493        Ok(receipt)
494    }
495
496    /// Produces a proof for the chain up to the block with the given hash.
497    ///
498    /// # Returns
499    ///
500    /// - [`Receipt`]: Specified block's proof receipt
501    /// - [`u64`]: Height of the proven header chain
502    pub async fn prove_till_hash(
503        &self,
504        block_hash: BlockHash,
505    ) -> Result<(Receipt, u64), BridgeError> {
506        let (_, _, height) = self
507            .db
508            .get_block_info_from_hash_hcp(None, block_hash)
509            .await?
510            .ok_or(eyre::eyre!("Block not found in prove_till_hash"))?;
511
512        let latest_proven_block = self
513            .db
514            .get_latest_proven_block_info_until_height(None, height)
515            .await?
516            .ok_or_eyre("No proofs found before the given block hash")?;
517
518        if latest_proven_block.2 == height as u64 {
519            let receipt = self
520                .db
521                .get_block_proof_by_hash(None, latest_proven_block.0)
522                .await
523                .wrap_err("Failed to get block proof")?
524                .ok_or(eyre!("Failed to get block proof"))?;
525            return Ok((receipt, height as u64));
526        }
527
528        let block_headers = self
529            .db
530            .get_block_info_from_range(None, latest_proven_block.2 + 1, height.into())
531            .await?
532            .into_iter()
533            .map(|(_hash, header)| header)
534            .collect::<Vec<_>>();
535
536        let previous_proof = self
537            .db
538            .get_block_proof_by_hash(None, latest_proven_block.0)
539            .await?
540            .ok_or(eyre::eyre!("No proven block found"))?;
541        let receipt = self
542            .prove_and_save_block(block_hash, block_headers, previous_proof)
543            .await?;
544        tracing::info!("Generated new proof for height {}", height);
545        Ok((receipt, height as u64))
546    }
547
548    /// Gets the proof of the latest finalized blockchain tip. If the finalized
549    /// blockchain tip isn't yet proven, it will be proven first in batches
550    /// (last proven block in database to finalized blockchain tip).
551    ///
552    /// # Returns
553    ///
554    /// - [`Receipt`]: Specified block's proof receipt
555    /// - [`u64`]: Height of the proven header chain
556    pub async fn get_tip_header_chain_proof(&self) -> Result<(Receipt, u64), BridgeError> {
557        let max_height = self.db.get_latest_finalized_block_height(None).await?;
558
559        if let Some(max_height) = max_height {
560            let block_hash = self
561                .db
562                .get_block_info_from_range(None, max_height, max_height)
563                .await?
564                .into_iter()
565                .next()
566                .expect("Block should be in table")
567                .0;
568            Ok(self.prove_till_hash(block_hash).await?)
569        } else {
570            Err(eyre::eyre!("No finalized blocks in header chain proofs table").into())
571        }
572    }
573
574    /// Saves a new block to database, later to be proven.
575    pub async fn save_unproven_block_cache(
576        &self,
577        dbtx: Option<DatabaseTransaction<'_>>,
578        block_cache: &BlockCache,
579    ) -> Result<(), BridgeError> {
580        let block_hash = block_cache.block.block_hash();
581
582        let block_header = block_cache.block.header;
583
584        self.db
585            .save_unproven_finalized_block(
586                dbtx,
587                block_hash,
588                block_header,
589                block_cache.block_height.into(),
590            )
591            .await?;
592
593        Ok(())
594    }
595
596    /// Checks if there are enough blocks to prove.
597    #[tracing::instrument(skip_all)]
598    async fn is_batch_ready(&self) -> Result<bool, BridgeError> {
599        let non_proven_block = if let Some(block) = self.db.get_next_unproven_block(None).await? {
600            block
601        } else {
602            return Ok(false);
603        };
604        let tip_height = self
605            .db
606            .get_latest_finalized_block_height(None)
607            .await?
608            .ok_or(eyre::eyre!("No tip block found"))?;
609
610        tracing::debug!(
611            "Tip height: {}, non proven block height: {}, {}",
612            tip_height,
613            non_proven_block.2,
614            self.batch_size
615        );
616        if tip_height - non_proven_block.2 + 1 >= self.batch_size {
617            return Ok(true);
618        }
619        tracing::debug!(
620            "Batch not ready: {} - {} < {}",
621            tip_height,
622            non_proven_block.2,
623            self.batch_size
624        );
625
626        Ok(false)
627    }
628
629    /// Proves blocks if the batch is ready. If not, skips.
630    pub async fn prove_if_ready(&self) -> Result<Option<Receipt>, BridgeError> {
631        if !self.is_batch_ready().await? {
632            return Ok(None);
633        }
634
635        let unproven_blocks = self
636            .db
637            .get_next_n_non_proven_block(
638                self.batch_size
639                    .try_into()
640                    .wrap_err("Can't convert u64 to u32")?,
641            )
642            .await?;
643        let (unproven_blocks, prev_proof) = match unproven_blocks {
644            Some(unproven_blocks) => unproven_blocks,
645            None => {
646                tracing::debug!("No unproven blocks found");
647                return Ok(None);
648            }
649        };
650
651        let current_block_hash = unproven_blocks.iter().next_back().expect("Exists").0;
652        let current_block_height = unproven_blocks.iter().next_back().expect("Exists").2;
653        let block_headers = unproven_blocks
654            .iter()
655            .map(|(_, header, _)| *header)
656            .collect::<Vec<_>>();
657
658        let receipt = self
659            .prove_and_save_block(current_block_hash, block_headers, prev_proof)
660            .await?;
661        tracing::info!(
662            "Header chain proof generated for block with hash {:?} and height {}",
663            current_block_hash,
664            current_block_height,
665        );
666
667        Ok(Some(receipt))
668    }
669}
670
671fn get_hcp_method_id(network: Network) -> Result<[u32; 8], HeaderChainProverError> {
672    match network {
673        Network::Bitcoin => Ok(*MAINNET_HCP_METHOD_ID),
674        Network::Testnet4 => Ok(*TESTNET4_HCP_METHOD_ID),
675        Network::Signet => Ok(*SIGNET_HCP_METHOD_ID),
676        Network::Regtest => Ok(*REGTEST_HCP_METHOD_ID),
677        _ => Err(HeaderChainProverError::UnsupportedNetwork),
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use crate::extended_bitcoin_rpc::ExtendedBitcoinRpc;
684    use crate::header_chain_prover::HeaderChainProver;
685    use crate::test::common::*;
686    use crate::verifier::VerifierServer;
687    use crate::{database::Database, test::common::citrea::MockCitreaClient};
688    use bitcoin::{block::Header, hashes::Hash, BlockHash, Network};
689    use bitcoincore_rpc::RpcApi;
690    use circuits_lib::header_chain::{
691        mmr_guest::MMRGuest, BlockHeaderCircuitOutput, ChainState, CircuitBlockHeader,
692    };
693    use secp256k1::rand::{self, Rng};
694
695    /// Mines `block_num` amount of blocks (if not already mined) and returns
696    /// the first `block_num` block headers in blockchain.
697    async fn mine_and_get_first_n_block_headers(
698        rpc: ExtendedBitcoinRpc,
699        db: Database,
700        block_num: u64,
701    ) -> Vec<Header> {
702        let height = rpc.get_block_count().await.unwrap();
703        tracing::debug!(
704            "Current tip height: {}, target block height: {}",
705            height,
706            block_num
707        );
708        if height < block_num {
709            tracing::debug!(
710                "Mining {} blocks to reach block number {}",
711                block_num - height,
712                block_num
713            );
714            rpc.mine_blocks(block_num - height + 1).await.unwrap();
715        }
716
717        tracing::debug!("Getting first {} block headers from blockchain", block_num);
718        let mut headers = Vec::new();
719        for i in 0..block_num + 1 {
720            let hash = rpc.get_block_hash(i).await.unwrap();
721            let header = rpc.get_block_header(&hash).await.unwrap();
722
723            headers.push(header);
724
725            let _ignore_errors = db
726                .save_unproven_finalized_block(None, hash, header, i)
727                .await;
728        }
729
730        headers
731    }
732
733    #[ignore = "This test is requires env var at build time, but it works, try it out"]
734    #[tokio::test]
735    #[serial_test::serial]
736    async fn test_generate_chain_state_from_height() {
737        // set BITCOIN_NETWORK to regtest
738        std::env::set_var("BITCOIN_NETWORK", "regtest");
739        let mut config = create_test_config_with_thread_name().await;
740        let regtest = create_regtest_rpc(&mut config).await;
741        let rpc = regtest.rpc().clone();
742        let db = Database::new(&config).await.unwrap();
743
744        // randomly select a number of blocks from 12 to 2116
745        let num_blocks: u64 = rand::rng().random_range(12..2116);
746
747        // Save some initial blocks.
748        let headers = mine_and_get_first_n_block_headers(rpc.clone(), db.clone(), num_blocks).await;
749
750        let chain_state =
751            HeaderChainProver::get_chain_state_from_height(&rpc, num_blocks, Network::Regtest)
752                .await
753                .unwrap();
754
755        let mut expected_chain_state = ChainState::genesis_state();
756        expected_chain_state.apply_block_headers(
757            headers
758                .iter()
759                .map(|header| CircuitBlockHeader::from(*header))
760                .collect::<Vec<_>>(),
761        );
762
763        expected_chain_state.block_hashes_mmr = MMRGuest::new();
764
765        println!("Chain state: {chain_state:#?}");
766        println!("Expected chain state: {expected_chain_state:#?}");
767
768        assert_eq!(chain_state, expected_chain_state);
769    }
770
771    #[ignore = "This test requires env var at build time & testnet4, but it works, try it out"]
772    #[tokio::test]
773    #[serial_test::serial]
774    async fn test_generate_chain_state_from_height_testnet4() {
775        // set BITCOIN_NETWORK to testnet4
776        std::env::set_var("BITCOIN_NETWORK", "testnet4");
777        let rpc = ExtendedBitcoinRpc::connect(
778            "http://127.0.0.1:48332".to_string(),
779            "admin".to_string().into(),
780            "admin".to_string().into(),
781            None,
782        )
783        .await
784        .unwrap();
785
786        // randomly select a number of blocks from 12 to 2116
787        let num_blocks: u64 = rand::rng().random_range(12..2116);
788
789        // Save some initial blocks.
790        let mut headers = Vec::new();
791        for i in 0..=num_blocks {
792            let hash = rpc.get_block_hash(i).await.unwrap();
793            let header = rpc.get_block_header(&hash).await.unwrap();
794            headers.push(header);
795        }
796
797        let chain_state =
798            HeaderChainProver::get_chain_state_from_height(&rpc, num_blocks, Network::Testnet4)
799                .await
800                .unwrap();
801
802        let mut expected_chain_state = ChainState::genesis_state();
803        expected_chain_state.apply_block_headers(
804            headers
805                .iter()
806                .map(|header| CircuitBlockHeader::from(*header))
807                .collect::<Vec<_>>(),
808        );
809
810        expected_chain_state.block_hashes_mmr = MMRGuest::new();
811
812        println!("Chain state: {chain_state:#?}");
813        println!("Expected chain state: {expected_chain_state:#?}");
814
815        assert_eq!(chain_state, expected_chain_state);
816    }
817
818    #[tokio::test]
819    async fn test_fetch_and_save_missing_blocks() {
820        // test these functions:
821        // save_block_infos_within_range
822        // fetch_and_save_missing_blocks
823        // get_block_info_from_hash_hcp
824        // get_latest_proven_block_info_until_height
825        let mut config = create_test_config_with_thread_name().await;
826        let regtest = create_regtest_rpc(&mut config).await;
827        let rpc = regtest.rpc().clone();
828
829        let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
830            .await
831            .unwrap();
832
833        let current_height = rpc.get_block_count().await.unwrap();
834        let current_hcp_height = prover
835            .db
836            .get_latest_finalized_block_height(None)
837            .await
838            .unwrap()
839            .unwrap();
840        assert_ne!(current_height, current_hcp_height);
841
842        prover
843            .db
844            .fetch_and_save_missing_blocks(
845                None,
846                &rpc,
847                config.protocol_paramset().genesis_height,
848                current_height as u32 + 1,
849            )
850            .await
851            .unwrap();
852
853        let current_hcp_height = prover
854            .db
855            .get_latest_finalized_block_height(None)
856            .await
857            .unwrap()
858            .unwrap();
859        assert_eq!(current_height, current_hcp_height);
860
861        let test_height = current_height as u32 / 2;
862
863        let block_hash = rpc.get_block_hash(test_height as u64).await.unwrap();
864        let block_info = prover
865            .db
866            .get_block_info_from_hash_hcp(None, block_hash)
867            .await
868            .unwrap()
869            .unwrap();
870        assert_eq!(block_info.2, test_height);
871
872        let receipt_1 = prover.prove_till_hash(block_hash).await.unwrap();
873        let latest_proven_block = prover
874            .db
875            .get_latest_proven_block_info_until_height(None, current_hcp_height as u32)
876            .await
877            .unwrap()
878            .unwrap();
879
880        let receipt_2 = prover.prove_till_hash(block_hash).await.unwrap();
881
882        assert_eq!(receipt_1.0.journal, receipt_2.0.journal);
883
884        assert_eq!(latest_proven_block.2, test_height as u64);
885    }
886
887    #[tokio::test]
888    async fn new() {
889        let mut config = create_test_config_with_thread_name().await;
890        let regtest = create_regtest_rpc(&mut config).await;
891        let rpc = regtest.rpc().clone();
892
893        let _should_not_panic = HeaderChainProver::new(&config, rpc).await.unwrap();
894    }
895
896    #[tokio::test]
897    async fn new_with_proof_assumption() {
898        let mut config = create_test_config_with_thread_name().await;
899        let regtest = create_regtest_rpc(&mut config).await;
900        let rpc = regtest.rpc().clone();
901
902        // First block's assumption will be added to db: Make sure block exists
903        // too.
904        rpc.mine_blocks(1).await.unwrap();
905        let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
906            .await
907            .unwrap();
908
909        // Test assumption is for block 0.
910        let hash = rpc.get_block_hash(0).await.unwrap();
911        let (receipt, _) = prover.prove_till_hash(hash).await.unwrap();
912        let db_receipt = prover
913            .db
914            .get_block_proof_by_hash(None, hash)
915            .await
916            .unwrap()
917            .unwrap();
918        assert_eq!(receipt.journal, db_receipt.journal);
919        assert_eq!(receipt.metadata, db_receipt.metadata);
920    }
921
922    #[tokio::test]
923    async fn prove_a_block_from_database() {
924        let mut config = create_test_config_with_thread_name().await;
925        let regtest = create_regtest_rpc(&mut config).await;
926        let rpc = regtest.rpc().clone();
927        let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
928            .await
929            .unwrap();
930
931        // Set up the next non proven block.
932        let height = 1;
933        let hash = rpc.get_block_hash(height).await.unwrap();
934        let genesis_hash = rpc.get_block_hash(0).await.unwrap();
935        let (genesis_receipt, _) = prover.prove_till_hash(genesis_hash).await.unwrap();
936        let block = rpc.get_block(&hash).await.unwrap();
937        let header = block.header;
938        prover
939            .db
940            .save_unproven_finalized_block(None, hash, header, height)
941            .await
942            .unwrap();
943
944        let receipt = prover
945            .prove_and_save_block(hash, vec![header], genesis_receipt)
946            .await
947            .unwrap();
948
949        let (read_recipt, _) = prover.prove_till_hash(hash).await.unwrap();
950        assert_eq!(receipt.journal, read_recipt.journal);
951    }
952
953    #[tokio::test]
954    #[serial_test::serial]
955    async fn prove_block_headers_genesis() {
956        let genesis_state = ChainState::genesis_state();
957
958        let receipt =
959            HeaderChainProver::prove_genesis_block(genesis_state, Network::Regtest).unwrap();
960
961        let output: BlockHeaderCircuitOutput = borsh::from_slice(&receipt.journal.bytes).unwrap();
962        println!("Proof journal output: {output:?}");
963
964        assert_eq!(output.chain_state.block_height, u32::MAX); // risc0-to-bitvm2 related
965        assert_eq!(
966            output.chain_state.best_block_hash,
967            BlockHash::all_zeros().as_raw_hash().to_byte_array()
968        );
969    }
970
971    #[tokio::test]
972    #[serial_test::serial]
973    async fn prove_block_headers_second() {
974        let mut config = create_test_config_with_thread_name().await;
975        let regtest = create_regtest_rpc(&mut config).await;
976        let rpc = regtest.rpc().clone();
977        let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
978            .await
979            .unwrap();
980
981        // Prove genesis block and get it's receipt.
982        let genesis_state = ChainState::genesis_state();
983
984        let receipt =
985            HeaderChainProver::prove_genesis_block(genesis_state, Network::Regtest).unwrap();
986
987        let block_headers = mine_and_get_first_n_block_headers(rpc, prover.db.clone(), 3)
988            .await
989            .iter()
990            .map(|header| CircuitBlockHeader::from(*header))
991            .collect::<Vec<_>>();
992        let receipt = HeaderChainProver::prove_block_headers(
993            prover.network,
994            receipt,
995            block_headers[0..2].to_vec(),
996        )
997        .unwrap();
998        let output: BlockHeaderCircuitOutput = borsh::from_slice(&receipt.journal.bytes).unwrap();
999
1000        println!("Proof journal output: {output:?}");
1001
1002        assert_eq!(output.chain_state.block_height, 1);
1003    }
1004
1005    #[tokio::test]
1006    async fn prove_till_hash_intermediate_blocks() {
1007        // this test does assume config start height is bigger than 3
1008        let mut config = create_test_config_with_thread_name().await;
1009        let regtest = create_regtest_rpc(&mut config).await;
1010        let rpc = regtest.rpc().clone();
1011        let db = Database::new(&config).await.unwrap();
1012
1013        let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
1014            .await
1015            .unwrap();
1016
1017        for i in (0..3).rev() {
1018            let hash = rpc.get_block_hash(i).await.unwrap();
1019            let (proof, _) = prover.prove_till_hash(hash).await.unwrap();
1020            let db_proof = db
1021                .get_block_proof_by_hash(None, hash)
1022                .await
1023                .unwrap()
1024                .unwrap();
1025            assert_eq!(proof.journal, db_proof.journal);
1026        }
1027        let hash = rpc.get_block_hash(5).await.unwrap();
1028        let (proof, _) = prover.prove_till_hash(hash).await.unwrap();
1029        let db_proof = db
1030            .get_block_proof_by_hash(None, hash)
1031            .await
1032            .unwrap()
1033            .unwrap();
1034        assert_eq!(proof.journal, db_proof.journal);
1035    }
1036
1037    #[tokio::test]
1038    async fn is_batch_ready() {
1039        let mut config = create_test_config_with_thread_name().await;
1040        let regtest = create_regtest_rpc(&mut config).await;
1041        let rpc = regtest.rpc().clone();
1042        let db = Database::new(&config).await.unwrap();
1043
1044        let batch_size = config.header_chain_proof_batch_size;
1045
1046        let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
1047            .await
1048            .unwrap();
1049
1050        let genesis_hash = rpc.get_block_hash(0).await.unwrap();
1051        let (genesis_block_proof, _) = prover.prove_till_hash(genesis_hash).await.unwrap();
1052        let db_proof = db
1053            .get_block_proof_by_hash(None, genesis_hash)
1054            .await
1055            .unwrap()
1056            .unwrap();
1057        assert_eq!(genesis_block_proof.journal, db_proof.journal);
1058
1059        assert!(
1060            prover.is_batch_ready().await.unwrap()
1061                == (config.protocol_paramset().start_height > batch_size)
1062        );
1063
1064        // Mining required amount of blocks should make batch proving ready.
1065        let _headers =
1066            mine_and_get_first_n_block_headers(rpc.clone(), db, batch_size as u64 + 1).await;
1067        assert!(prover.is_batch_ready().await.unwrap());
1068    }
1069
1070    #[tokio::test]
1071    async fn prove_if_ready() {
1072        let mut config = create_test_config_with_thread_name().await;
1073        let regtest = create_regtest_rpc(&mut config).await;
1074        let rpc = regtest.rpc().clone();
1075        let db = Database::new(&config).await.unwrap();
1076
1077        let prover = HeaderChainProver::new(&config, rpc.clone()).await.unwrap();
1078
1079        // Save some initial blocks.
1080        mine_and_get_first_n_block_headers(rpc.clone(), db.clone(), 2).await;
1081
1082        let batch_size = config.header_chain_proof_batch_size;
1083
1084        let latest_proven_block_height = db.get_next_unproven_block(None).await.unwrap().unwrap().2;
1085        let _block_headers = mine_and_get_first_n_block_headers(
1086            rpc.clone(),
1087            db.clone(),
1088            latest_proven_block_height + batch_size as u64,
1089        )
1090        .await;
1091
1092        let receipt = prover.prove_if_ready().await.unwrap().unwrap();
1093        let latest_proof = db
1094            .get_latest_proven_block_info(None)
1095            .await
1096            .unwrap()
1097            .unwrap();
1098        let get_receipt = prover
1099            .db
1100            .get_block_proof_by_hash(None, latest_proof.0)
1101            .await
1102            .unwrap()
1103            .unwrap();
1104        assert_eq!(receipt.journal, get_receipt.journal);
1105        assert_eq!(receipt.metadata, get_receipt.metadata);
1106    }
1107
1108    #[tokio::test]
1109    async fn prove_and_get_non_targeted_block() {
1110        let mut config = create_test_config_with_thread_name().await;
1111        let regtest = create_regtest_rpc(&mut config).await;
1112        let rpc = regtest.rpc().clone();
1113        let db = Database::new(&config).await.unwrap();
1114
1115        let prover = HeaderChainProver::new(&config, rpc.clone()).await.unwrap();
1116
1117        // Save some initial blocks.
1118        mine_and_get_first_n_block_headers(rpc.clone(), db.clone(), 2).await;
1119
1120        let batch_size = config.header_chain_proof_batch_size;
1121
1122        let latest_proven_block_height = db.get_next_unproven_block(None).await.unwrap().unwrap().2;
1123        let _block_headers = mine_and_get_first_n_block_headers(
1124            rpc.clone(),
1125            db.clone(),
1126            latest_proven_block_height + batch_size as u64,
1127        )
1128        .await;
1129
1130        let receipt = prover.prove_if_ready().await.unwrap().unwrap();
1131        let latest_proof = db
1132            .get_latest_proven_block_info(None)
1133            .await
1134            .unwrap()
1135            .unwrap();
1136        let get_receipt = prover
1137            .db
1138            .get_block_proof_by_hash(None, latest_proof.0)
1139            .await
1140            .unwrap()
1141            .unwrap();
1142        assert_eq!(receipt.journal, get_receipt.journal);
1143        assert_eq!(receipt.metadata, get_receipt.metadata);
1144
1145        // Try to get proof of the previous block that its heir is proven.
1146        let target_height = latest_proof.2 - 1;
1147        let target_hash = rpc.get_block_hash(target_height).await.unwrap();
1148
1149        assert!(db
1150            .get_block_proof_by_hash(None, target_hash)
1151            .await
1152            .unwrap()
1153            .is_none());
1154
1155        // get_header_chain_proof should calculate the proof for the block.
1156        let _receipt = prover.get_tip_header_chain_proof().await.unwrap();
1157    }
1158
1159    #[tokio::test]
1160    #[cfg(feature = "automation")]
1161    async fn verifier_new_check_header_chain_proof() {
1162        let mut config = create_test_config_with_thread_name().await;
1163        let regtest = create_regtest_rpc(&mut config).await;
1164        let rpc = regtest.rpc().clone();
1165        let db = Database::new(&config).await.unwrap();
1166
1167        let batch_size = config.header_chain_proof_batch_size;
1168
1169        // Save initial blocks, because VerifierServer won't.
1170        let count = rpc.get_block_count().await.unwrap();
1171        tracing::info!("Block count: {}", count);
1172        for i in 1..count + 1 {
1173            let hash = rpc.get_block_hash(i).await.unwrap();
1174            let block = rpc.get_block(&hash).await.unwrap();
1175
1176            db.save_unproven_finalized_block(None, block.block_hash(), block.header, i)
1177                .await
1178                .unwrap();
1179        }
1180
1181        let verifier = VerifierServer::<MockCitreaClient>::new(config)
1182            .await
1183            .unwrap();
1184        verifier.start_background_tasks().await.unwrap();
1185        // Make sure enough blocks to prove and is finalized.
1186        rpc.mine_blocks((batch_size + 10).into()).await.unwrap();
1187
1188        // Aim for a proved block that is added to the database by the verifier.
1189        let height = batch_size;
1190        let hash = rpc.get_block_hash(height.into()).await.unwrap();
1191
1192        poll_until_condition(
1193            async || {
1194                Ok(verifier
1195                    .verifier
1196                    .header_chain_prover
1197                    .db
1198                    .get_block_proof_by_hash(None, hash)
1199                    .await
1200                    .is_ok())
1201            },
1202            None,
1203            None,
1204        )
1205        .await
1206        .unwrap();
1207    }
1208}