Skip to main content

binius_circuits/bitcoin/
header_chain.rs

1// Copyright 2025 Irreducible Inc.
2
3//! Verifying a Bitcoin header chain.
4
5use binius_core::Word;
6use binius_frontend::{CircuitBuilder, Wire};
7
8use crate::{
9	bignum::{self, BigUint},
10	bitcoin::double_sha256::double_sha256,
11};
12
13/// Returns `hash(headers[0])`, the digest of the latest block in the chain.
14///
15/// Asserts the following things:
16///
17/// - `previous_block_hash(headers[i]) = hash(headers[i+1])` (hash chain)
18/// - `hash(headers[i]) < target(headers[i])` (proof of work)
19///
20/// The digest of the head is returned rather than asserted, so the caller decides what to
21/// compare it against.
22///
23/// **IMPORTANT**: This does currently NOT assert that `target(headers[i])` is in any way
24/// related to `target(headers[i+1])`, like it must be in the Bitcoin protocol. In particular,
25/// this means that one can easily satisfy this circuit with a self-mined sequence of blocks
26/// which have low difficulty.
27///
28/// **Note**: It also doesn't check many other things about the block header, like that the
29/// constraints on the timestamps.
30///
31/// # Panics
32///
33/// If `headers` is empty.
34pub fn header_chain(
35	builder: &CircuitBuilder,
36	// latest block comes first
37	headers: &[[Wire; 10]],
38) -> [Wire; 4] {
39	let latest_digest = double_sha256(builder, &headers[0]);
40	assert_fulfills_target(builder, latest_digest, headers[0][9]);
41
42	for i in 1..headers.len() {
43		// hash equals the "previous block hash" in the newer block
44		let digest = double_sha256(builder, &headers[i]);
45		builder.assert_eq_v("hash chain", digest, previous_block_hash(builder, &headers[i - 1]));
46
47		// NOTE: One could save constraints by noting that the target only changes every 2016
48		// blocks.
49		assert_fulfills_target(builder, digest, headers[i][9]);
50
51		// bits of newer block is correctly computed from bits of older block
52		// FIXME TODO
53	}
54
55	latest_digest
56}
57
58/// Asserts the proof of work: the digest, read as a 256-bit integer, is below the header's target.
59fn assert_fulfills_target(builder: &CircuitBuilder, digest: [Wire; 4], bits: Wire) {
60	let target = target(builder, bits);
61	let digest_as_uint = BigUint {
62		limbs: digest.to_vec(),
63	};
64	let fulfills_target = bignum::biguint_lt(builder, &digest_as_uint, &target);
65	builder.assert_true("PoW fulfills target", fulfills_target);
66}
67
68/// Extracts the previous block hash from a block header.
69fn previous_block_hash(builder: &CircuitBuilder, header: &[Wire; 10]) -> [Wire; 4] {
70	[
71		join(builder, header[1], header[0]),
72		join(builder, header[2], header[1]),
73		join(builder, header[3], header[2]),
74		join(builder, header[4], header[3]),
75	]
76}
77
78fn join(builder: &CircuitBuilder, b0: Wire, b1: Wire) -> Wire {
79	let c0 = builder.shl(b0, 32);
80	let c1 = builder.shr(b1, 32);
81	builder.bxor(c0, c1)
82}
83
84/// Computes the 32-byte target from the 4-byte compact bits field.
85fn target(builder: &CircuitBuilder, bits: Wire) -> BigUint {
86	let mantissa = builder.band(builder.add_constant_64(0x0000000000ffffff), bits);
87	let exponent = builder.band(builder.add_constant_64(0x00000000000000ff), builder.shr(bits, 24));
88
89	// compute how many bytes we need to shift the mantissa to the left
90	// NOTE: Check underflow? And check that exponent is not too big?
91	let (shift_val, _) = builder.isub_bin_bout(
92		exponent,
93		builder.add_constant_64(3),
94		builder.add_constant(Word::ZERO),
95	);
96
97	// optionally shift by 1 byte
98	let cond_1 = builder.shl(shift_val, 63);
99	let v0_1 = builder.select(cond_1, builder.shl(mantissa, 8), mantissa);
100
101	// optionally shift by 2 bytes
102	let cond_2 = builder.shl(shift_val, 62);
103	let v0_2 = builder.select(cond_2, builder.shl(v0_1, 2 * 8), v0_1);
104
105	// optionally shift by 4 bytes
106	let cond_4 = builder.shl(shift_val, 61);
107	let v0_4 = builder.select(cond_4, builder.shl(v0_2, 4 * 8), v0_2);
108	let v1_4 = builder.select(cond_4, builder.shr(v0_2, 4 * 8), builder.add_constant(Word::ZERO));
109
110	// optionally shift by 8 bytes
111	let cond_8 = builder.shl(shift_val, 60);
112	let v0_8 = builder.select(cond_8, builder.add_constant(Word::ZERO), v0_4);
113	let v1_8 = builder.select(cond_8, v0_4, v1_4);
114	let v2_8 = builder.select(cond_8, v1_4, builder.add_constant(Word::ZERO));
115
116	// optionally shift by 16 bytes
117	let cond_16 = builder.shl(shift_val, 59);
118	let v0_16 = builder.select(cond_16, builder.add_constant(Word::ZERO), v0_8);
119	let v1_16 = builder.select(cond_16, builder.add_constant(Word::ZERO), v1_8);
120	let v2_16 = builder.select(cond_16, v0_8, v2_8);
121	let v3_16 = builder.select(cond_16, v1_8, builder.add_constant(Word::ZERO));
122
123	BigUint {
124		limbs: vec![v0_16, v1_16, v2_16, v3_16],
125	}
126}
127
128#[cfg(test)]
129mod tests {
130	use hex_literal::hex;
131
132	use super::*;
133	use crate::bignum;
134
135	/// Tests that the `target` circuit is correct.
136	fn test_target_fixed_exponent(exponent: usize) {
137		// build circuit
138		let builder = CircuitBuilder::new();
139		let bits = builder.add_witness();
140		let asserted_target = BigUint::new_witness(&builder, 4);
141		let target = target(&builder, bits);
142		bignum::assert_eq(&builder, "target matches asserted target", &target, &asserted_target);
143		let circuit = builder.build();
144
145		// populate witness
146		let mut filler = circuit.new_witness_filler();
147		let bits_value: u64 = 0x4444444400aabbcc | ((exponent as u64) << 24);
148		filler[bits] = Word(bits_value);
149		let shift_val = exponent - 3;
150		let asserted_target_value = num_bigint::BigUint::from_bytes_be(&hex!(
151			"0000000000000000000000000000000000000000000000000000000000aabbcc"
152		)) << (shift_val * 8);
153		let mut asserted_target_digits = asserted_target_value.to_u64_digits();
154		asserted_target_digits.truncate(4);
155		while asserted_target_digits.len() < 4 {
156			asserted_target_digits.push(0);
157		}
158		asserted_target.populate_limbs(&mut filler, &asserted_target_digits);
159		circuit.populate_wire_witness(&mut filler).unwrap();
160
161		// check
162		let constraint_system = circuit.constraint_system();
163		constraint_system.verify(&filler.into_value_vec()).unwrap();
164	}
165
166	#[test]
167	fn test_target() {
168		// `exponent - 3` runs in 0..32
169		for exponent in 3..35 {
170			test_target_fixed_exponent(exponent);
171		}
172	}
173
174	#[test]
175	fn test_valid() {
176		// build circuit
177		let builder = CircuitBuilder::new();
178		let headers: Vec<[Wire; 10]> =
179			std::iter::repeat_with(|| std::array::from_fn(|_| builder.add_witness()))
180				.take(3)
181				.collect();
182		let latest_digest: [Wire; 4] = std::array::from_fn(|_| builder.add_witness());
183		builder.assert_eq_v("latest digest", header_chain(&builder, &headers), latest_digest);
184		let circuit = builder.build();
185
186		// populate witness
187		let mut filler = circuit.new_witness_filler();
188		let headers_value = vec![
189			hex!(
190				"000000264a14e21adad047d981c06a26446e345eda3d8beb807401000000000000000000fc01df2139954b36cebc3fa6fbf6a7160a67d34b67e5c4aa2a7ce46f5bb42a83642ea468b32c0217d14ba4d1"
191			),
192			hex!(
193				"00606a3190af271dec0197c6b87f218070c5611cf0c506f6671c0200000000000000000021f21732014796558e6736b15de9b132ca65318b42fe5759b153c63dd4306e38842da468b32c021737421730"
194			),
195			hex!(
196				"00800020e77cf8eb3114116cc2e6d4aca9b27d35bb5402a5054a010000000000000000005b41d83c40c8226c48807401b0b08aa8e4e2eb053bf91d580f62cd49f5c2a99f802ba468b32c0217cea24c34"
197			),
198		];
199		let latest_digest_value =
200			hex!("228561b085b7524957e515605725901238299ff2793300000000000000000000");
201		for (header, header_value) in headers.iter().zip(&headers_value) {
202			filler.pack_bytes_le(header, header_value);
203		}
204		filler.pack_bytes_le(&latest_digest, &latest_digest_value);
205		circuit.populate_wire_witness(&mut filler).unwrap();
206
207		// check
208		let constraint_system = circuit.constraint_system();
209		constraint_system.verify(&filler.into_value_vec()).unwrap();
210	}
211}