Skip to main content

binius_examples/circuits/
bitcoin_header_chain.rs

1// Copyright 2025 Irreducible Inc.
2
3use anyhow::bail;
4use binius_circuits::bitcoin::header_chain::header_chain;
5use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
6use clap::Args;
7
8use crate::ExampleCircuit;
9
10/// Check some basic things about a Bitcoin header chain.
11///
12/// **IMPORTANT**: This does **NOT** prove full validity of the header chain.
13/// In particular it's not checked _at all_ that the target difficulty is calculated correctly.
14/// One could easily satisfy this circuit by quickly self-mining many blocks with low difficulty.
15pub struct BitcoinHeaderChainExample {
16	latest_digest: [Wire; 4],
17	headers: Vec<[Wire; 10]>,
18}
19
20#[derive(Args)]
21pub struct Params {
22	/// Number of block headers to prove.
23	#[arg(long, default_value_t = 10)]
24	pub num_blocks: usize,
25}
26
27#[derive(Args)]
28pub struct Instance {
29	/// Height of the newest block in the proved header chain.
30	/// Defaults to the currently latest block in the network.
31	#[arg(long)]
32	pub to_block: Option<usize>,
33}
34
35impl ExampleCircuit for BitcoinHeaderChainExample {
36	type Params = Params;
37	type Instance = Instance;
38
39	fn build(params: Self::Params, builder: &mut CircuitBuilder) -> anyhow::Result<Self> {
40		if params.num_blocks == 0 {
41			bail!("need to prove at least one block")
42		}
43
44		let latest_digest: [Wire; 4] = std::array::from_fn(|_| builder.add_witness());
45		let headers: Vec<[Wire; 10]> =
46			std::iter::repeat_with(|| std::array::from_fn(|_| builder.add_witness()))
47				.take(params.num_blocks)
48				.collect();
49		builder.assert_eq_v("latest digest", header_chain(builder, &headers), latest_digest);
50
51		Ok(Self {
52			latest_digest,
53			headers,
54		})
55	}
56
57	fn populate_witness(
58		&self,
59		instance: Self::Instance,
60		filler: &mut WitnessFiller<'_>,
61	) -> anyhow::Result<()> {
62		let (headers_value, latest_digest_value) =
63			pull_headers(self.headers.len(), instance.to_block)?;
64		for (header, header_value) in self.headers.iter().zip(&headers_value) {
65			filler.pack_bytes_le(header, header_value);
66		}
67		filler.pack_bytes_le(&self.latest_digest, &latest_digest_value);
68
69		Ok(())
70	}
71
72	fn param_summary(params: &Self::Params) -> Option<String> {
73		Some(format!("{}blk", params.num_blocks))
74	}
75}
76
77fn pull_headers(
78	num_blocks: usize,
79	to_block: Option<usize>,
80) -> anyhow::Result<(Vec<Vec<u8>>, Vec<u8>)> {
81	let last_height: usize = match to_block {
82		Some(to_block) => to_block,
83		None => ureq::get("https://mempool.space/api/blocks/tip/height")
84			.call()?
85			.body_mut()
86			.read_to_string()?
87			.trim()
88			.parse()?,
89	};
90
91	let latest_digest = ureq::get(format!("https://mempool.space/api/block-height/{last_height}"))
92		.call()?
93		.body_mut()
94		.read_to_string()?;
95	let mut latest_digest = hex::decode(latest_digest)?;
96	latest_digest.reverse();
97
98	let first_height = (last_height + 1).checked_sub(num_blocks).unwrap();
99	println!(
100		"Fetching {num_blocks} block headers. First height is {first_height} and last height is {last_height} .."
101	);
102	let mut headers = Vec::new();
103	for height in (first_height..=last_height).rev() {
104		let hash = ureq::get(format!("https://mempool.space/api/block-height/{height}"))
105			.call()?
106			.body_mut()
107			.read_to_string()?;
108		println!("Fetching header with height {height} and hash {hash} ..");
109		let header = ureq::get(format!("https://mempool.space/api/block/{hash}/header"))
110			.call()?
111			.body_mut()
112			.read_to_string()?;
113		let header = hex::decode(header)?;
114		headers.push(header);
115	}
116
117	println!("Done.");
118
119	Ok((headers, latest_digest))
120}