1use 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 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 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 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 ); 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 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 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 #[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 fn prove_block_headers(
424 network: Network,
425 prev_receipt: Receipt,
426 block_headers: Vec<CircuitBlockHeader>,
427 ) -> Result<Receipt, HeaderChainProverError> {
428 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 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 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 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 #[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 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 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 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 let num_blocks: u64 = rand::rng().random_range(12..2116);
746
747 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 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 let num_blocks: u64 = rand::rng().random_range(12..2116);
788
789 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 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 rpc.mine_blocks(1).await.unwrap();
905 let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
906 .await
907 .unwrap();
908
909 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 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); 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 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 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 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 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 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 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 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 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 rpc.mine_blocks((batch_size + 10).into()).await.unwrap();
1187
1188 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}