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}