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