1use 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 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 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 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 ); 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 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 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 #[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 fn prove_block_headers(
438 network: Network,
439 prev_receipt: Receipt,
440 block_headers: Vec<CircuitBlockHeader>,
441 ) -> Result<Receipt, HeaderChainProverError> {
442 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 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 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 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 #[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 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 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 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 let num_blocks: u64 = rand::rng().random_range(12..2116);
760
761 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 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 let num_blocks: u64 = rand::rng().random_range(12..2116);
802
803 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 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 rpc.mine_blocks(1).await.unwrap();
919 let prover = HeaderChainProver::new(&config, rpc.clone_inner().await.unwrap())
920 .await
921 .unwrap();
922
923 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 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); 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 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 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 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 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 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 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 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 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 rpc.mine_blocks((batch_size + 10).into()).await.unwrap();
1201
1202 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}