Skip to main content

binius_examples/circuits/
utils.rs

1// Copyright 2026 The Binius Developers
2// Copyright 2025 Irreducible Inc.
3//! Utilities for hash circuit examples
4
5use anyhow::{Result, bail, ensure};
6use binius_core::word::Word;
7use clap::Args;
8use rand::prelude::*;
9
10/// Default message size for hash circuit examples (1 KiB)
11///
12/// This value is chosen as a reasonable default that:
13/// - Is large enough to demonstrate performance characteristics
14/// - Small enough for quick testing and development
15/// - Aligns with common block sizes in many systems
16pub const DEFAULT_HASH_MESSAGE_BYTES: usize = 1024;
17
18/// Standard seed for reproducible random message generation
19pub const DEFAULT_RANDOM_SEED: u64 = 42;
20
21/// Circuit-shape parameters shared by every hasher example.
22///
23/// These are compile-time [`Params`](crate::ExampleCircuit::Params): they select which gadget the
24/// circuit is built from, independently of the message that is later hashed.
25///
26/// * `--message-len` builds the fixed-length gadget for exactly that many bytes.
27/// * `--max-message-len` builds the variable-length gadget with that capacity.
28/// * The two are mutually exclusive; if neither is given, the fixed-length gadget is built with
29///   [`DEFAULT_HASH_MESSAGE_BYTES`].
30#[derive(Args, Debug, Clone)]
31pub struct HasherParams {
32	/// Fixed message length in bytes; builds the more efficient fixed-length gadget. Mutually
33	/// exclusive with `--max-message-len`.
34	#[arg(long, conflicts_with = "max_message_len")]
35	pub message_len: Option<usize>,
36
37	/// Maximum message length in bytes; builds the variable-length gadget with this capacity.
38	/// Mutually exclusive with `--message-len`.
39	#[arg(long)]
40	pub max_message_len: Option<usize>,
41}
42
43/// Instance (witness) parameters shared by every hasher example.
44///
45/// These are proof-time [`Instance`](crate::ExampleCircuit::Instance) arguments: they choose the
46/// concrete message that is hashed by a circuit whose shape is already fixed by [`HasherParams`].
47///
48/// * `--message <STR>` hashes a UTF-8 string.
49/// * `--random-message` hashes reproducible random bytes; `--random-message-len <LEN>` sets the
50///   length (defaulting to the circuit's message length / maximum). It requires `--random-message`.
51/// * `--message` and `--random-message` are mutually exclusive; random is the default when neither
52///   is given.
53#[derive(Args, Debug, Clone)]
54pub struct HasherInstance {
55	/// Hash reproducible random bytes. This is the default when `--message` is not given.
56	#[arg(long, conflicts_with = "message")]
57	pub random_message: bool,
58
59	/// Length in bytes of the random message. Requires `--random-message`; defaults to the
60	/// circuit's fixed length (or maximum, for a variable-length circuit).
61	#[arg(long, requires = "random_message")]
62	pub random_message_len: Option<usize>,
63
64	/// Hash this UTF-8 string. Mutually exclusive with `--random-message`.
65	#[arg(long)]
66	pub message: Option<String>,
67}
68
69/// The gadget shape selected by [`HasherParams`].
70#[derive(Debug, Clone, Copy)]
71pub enum HasherMode {
72	/// Fixed-length gadget: the message length is a compile-time constant.
73	Fixed { len_bytes: usize },
74	/// Variable-length gadget: the message length is a runtime witness bounded by `max_len_bytes`.
75	Variable { max_len_bytes: usize },
76}
77
78impl HasherMode {
79	/// The default random-message length for this mode: the fixed length, or the maximum length.
80	pub const fn capacity_bytes(&self) -> usize {
81		match self {
82			HasherMode::Fixed { len_bytes } => *len_bytes,
83			HasherMode::Variable { max_len_bytes } => *max_len_bytes,
84		}
85	}
86}
87
88/// Resolve [`HasherParams`] into the gadget shape the circuit should be built from.
89///
90/// `supports_variable` is `false` for hashers that only have a fixed-length gadget (Blake2s,
91/// Blake2b, Blake3); passing `--max-message-len` to those is an error.
92pub fn resolve_hasher_mode(
93	params: &HasherParams,
94	hasher: &str,
95	supports_variable: bool,
96) -> Result<HasherMode> {
97	match (params.message_len, params.max_message_len) {
98		// `conflicts_with` makes clap reject this before we get here; guard anyway.
99		(Some(_), Some(_)) => {
100			bail!("--message-len and --max-message-len are mutually exclusive")
101		}
102		(Some(len_bytes), None) => Ok(HasherMode::Fixed { len_bytes }),
103		(None, Some(max_len_bytes)) => {
104			ensure!(
105				supports_variable,
106				"--max-message-len is not supported for {hasher}: only a fixed-length gadget exists"
107			);
108			Ok(HasherMode::Variable { max_len_bytes })
109		}
110		(None, None) => Ok(HasherMode::Fixed {
111			len_bytes: DEFAULT_HASH_MESSAGE_BYTES,
112		}),
113	}
114}
115
116/// Resolve a [`HasherInstance`] into the concrete message bytes to hash, validated against `mode`.
117///
118/// A `--message` string is used verbatim; otherwise reproducible random bytes are generated with
119/// length `--random-message-len` (defaulting to `mode`'s capacity). The resulting length must be
120/// compatible with the circuit: exactly the fixed length, or at most the variable-length maximum.
121pub fn resolve_hasher_message(mode: &HasherMode, instance: &HasherInstance) -> Result<Vec<u8>> {
122	let message = instance.message.as_ref().map_or_else(
123		|| {
124			let len = instance
125				.random_message_len
126				.unwrap_or_else(|| mode.capacity_bytes());
127			let mut rng = StdRng::seed_from_u64(DEFAULT_RANDOM_SEED);
128			let mut bytes = vec![0u8; len];
129			rng.fill_bytes(&mut bytes);
130			bytes
131		},
132		|s| s.clone().into_bytes(),
133	);
134
135	ensure!(!message.is_empty(), "Message length must be positive");
136	match mode {
137		HasherMode::Fixed { len_bytes } => ensure!(
138			message.len() == *len_bytes,
139			"message length ({}) must equal the fixed circuit length ({})",
140			message.len(),
141			len_bytes
142		),
143		HasherMode::Variable { max_len_bytes } => ensure!(
144			message.len() <= *max_len_bytes,
145			"message length ({}) exceeds the maximum ({})",
146			message.len(),
147			max_len_bytes
148		),
149	}
150
151	Ok(message)
152}
153
154/// One-line summary of the circuit shape selected by [`HasherParams`], for stat output.
155pub fn hasher_param_summary(params: &HasherParams) -> Option<String> {
156	Some(match (params.message_len, params.max_message_len) {
157		(_, Some(max)) => format!("var{max}b"),
158		(Some(len), None) => format!("{len}b"),
159		(None, None) => format!("{DEFAULT_HASH_MESSAGE_BYTES}b"),
160	})
161}
162
163/// Pack message bytes into 32-bit words, one word per wire with the high 32 bits zero.
164///
165/// The final word is zero-padded if the message length is not a multiple of 4. `big_endian`
166/// selects the byte order within each word.
167pub fn pack_bytes_u32words(message: &[u8], big_endian: bool) -> Vec<Word> {
168	let n_words = message.len().div_ceil(4);
169	(0..n_words)
170		.map(|i| {
171			let mut buf = [0u8; 4];
172			let start = i * 4;
173			let end = (start + 4).min(message.len());
174			buf[..end - start].copy_from_slice(&message[start..end]);
175			let word = if big_endian {
176				u32::from_be_bytes(buf)
177			} else {
178				u32::from_le_bytes(buf)
179			};
180			Word(word as u64)
181		})
182		.collect()
183}
184
185/// Pack message bytes into 64-bit words, one word per wire.
186///
187/// The final word is zero-padded if the message length is not a multiple of 8. `big_endian`
188/// selects the byte order within each word.
189pub fn pack_bytes_u64words(message: &[u8], big_endian: bool) -> Vec<Word> {
190	let n_words = message.len().div_ceil(8);
191	(0..n_words)
192		.map(|i| {
193			let mut buf = [0u8; 8];
194			let start = i * 8;
195			let end = (start + 8).min(message.len());
196			buf[..end - start].copy_from_slice(&message[start..end]);
197			let word = if big_endian {
198				u64::from_be_bytes(buf)
199			} else {
200				u64::from_le_bytes(buf)
201			};
202			Word(word)
203		})
204		.collect()
205}