Skip to main content

binius_circuits/bitcoin/
double_sha256.rs

1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3//! The Bitcoin double-SHA256 hash function.
4
5use binius_frontend::{CircuitBuilder, Wire};
6
7use crate::{bytes::swap_bytes_32, sha256::sha256_fixed, util::clear_high_bits};
8
9/// Returns `SHA256(SHA256(message))`.
10///
11/// The message length in bytes is fixed at compile time to be `message.len() * 8`.
12///
13/// `message` and the returned digest are Bitcoin little-endian packed (8 bytes per wire).
14pub fn double_sha256(builder: &CircuitBuilder, message: &[Wire]) -> [Wire; 4] {
15	// First SHA-256. `message` is little-endian 8-byte wires; `sha256_fixed` consumes
16	// big-endian 32-bit schedule words, so byte-swap within each 32-bit half and split each
17	// 64-bit wire into its two schedule words (mirrors `sha256::sha256_varlen`'s input
18	// prologue).
19	let mut message_be: Vec<Wire> = Vec::with_capacity(message.len() * 2);
20	for &w in message {
21		let swapped = swap_bytes_32(builder, w);
22		message_be.push(clear_high_bits(builder, swapped, 32));
23		message_be.push(builder.shr(swapped, 32));
24	}
25	let digest_0_be = sha256_fixed(builder, &message_be, message.len() * 8); // [Wire; 8] BE
26
27	// Second SHA-256 over the 32-byte first digest. Its output words are already the big-endian
28	// 32-bit schedule words the second hash expects, so feed them straight in (no swap).
29	let digest_1_be = sha256_fixed(builder, &digest_0_be, 32); // [Wire; 8] BE
30
31	// Repack the big-endian 32-bit output words into little-endian 64-bit wires, the form
32	// `merkle_path`/`header_chain` chain on.
33	std::array::from_fn(|i| {
34		let lo = swap_bytes_32(builder, digest_1_be[2 * i]);
35		let hi = swap_bytes_32(builder, digest_1_be[2 * i + 1]);
36		builder.bxor(lo, builder.shl(hi, 32))
37	})
38}
39
40#[cfg(test)]
41mod tests {
42	use std::array;
43
44	use hex_literal::hex;
45
46	use super::*;
47
48	/// Builds a circuit asserting `double_sha256(header) == hash`, then runs it on the given
49	/// values.
50	fn check_double_sha256(header_value: &[u8], hash_value: &[u8]) -> anyhow::Result<()> {
51		let builder = CircuitBuilder::new();
52		let block_header: [Wire; 10] = array::from_fn(|_| builder.add_witness());
53		let block_hash: [Wire; 4] = array::from_fn(|_| builder.add_witness());
54		builder.assert_eq_v("block hash", double_sha256(&builder, &block_header), block_hash);
55		let circuit = builder.build();
56
57		let mut filler = circuit.new_witness_filler();
58		filler.pack_bytes_le(&block_header, header_value);
59		filler.pack_bytes_le(&block_hash, hash_value);
60		circuit.populate_wire_witness(&mut filler)?;
61
62		let constraint_system = circuit.constraint_system();
63		constraint_system.verify(&filler.into_value_vec())?;
64		Ok(())
65	}
66
67	const BLOCK_HEADER: [u8; 80] = hex!(
68		"000000264a14e21adad047d981c06a26446e345eda3d8beb807401000000000000000000fc01df2139954b36cebc3fa6fbf6a7160a67d34b67e5c4aa2a7ce46f5bb42a83642ea468b32c0217d14ba4d1"
69	);
70
71	#[test]
72	fn test_valid() {
73		let block_hash = hex!("228561b085b7524957e515605725901238299ff2793300000000000000000000");
74		check_double_sha256(&BLOCK_HEADER, &block_hash).unwrap();
75	}
76
77	#[test]
78	fn test_invalid() {
79		let block_hash = hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
80		check_double_sha256(&BLOCK_HEADER, &block_hash).unwrap_err();
81	}
82}