binius_circuits/lasso/lookups/
u8_arithmetic.rs

1// Copyright 2024-2025 Irreducible Inc.
2
3use anyhow::Result;
4use binius_core::oracle::OracleId;
5use binius_field::{BinaryField32b, TowerField};
6
7use crate::builder::ConstraintSystemBuilder;
8
9type B32 = BinaryField32b;
10const T_LOG_SIZE_MUL: usize = 16;
11const T_LOG_SIZE_ADD: usize = 17;
12const T_LOG_SIZE_DCI: usize = 10;
13
14pub fn mul_lookup(
15	builder: &mut ConstraintSystemBuilder,
16	name: impl ToString + Clone,
17) -> Result<OracleId, anyhow::Error> {
18	builder.push_namespace(name);
19
20	let lookup_t = builder.add_committed("lookup_t", T_LOG_SIZE_MUL, B32::TOWER_LEVEL);
21
22	if let Some(witness) = builder.witness() {
23		let mut lookup_t = witness.new_column::<B32>(lookup_t);
24
25		let lookup_t_u32 = lookup_t.as_mut_slice::<u32>();
26
27		for (i, lookup_t) in lookup_t_u32.iter_mut().enumerate() {
28			let a_int = (i >> 8) & 0xff;
29			let b_int = i & 0xff;
30			let ab_product = a_int * b_int;
31			let lookup_index = a_int << 8 | b_int;
32			assert_eq!(lookup_index, i);
33			*lookup_t = (lookup_index << 16 | ab_product) as u32;
34		}
35	}
36
37	builder.pop_namespace();
38	Ok(lookup_t)
39}
40
41pub fn add_lookup(
42	builder: &mut ConstraintSystemBuilder,
43	name: impl ToString + Clone,
44) -> Result<OracleId, anyhow::Error> {
45	builder.push_namespace(name);
46
47	let lookup_t = builder.add_committed("lookup_t", T_LOG_SIZE_ADD, B32::TOWER_LEVEL);
48
49	if let Some(witness) = builder.witness() {
50		let mut lookup_t = witness.new_column::<B32>(lookup_t);
51
52		let lookup_t_u32 = lookup_t.as_mut_slice::<u32>();
53
54		for carry_in_usize in 0..(1 << 1) {
55			for x_in_usize in 0..(1 << 8) {
56				for y_in_usize in 0..(1 << 8) {
57					let lookup_index = (carry_in_usize << 16) | (x_in_usize << 8) | y_in_usize;
58					let xy_sum_with_carry_out = x_in_usize + y_in_usize + carry_in_usize;
59					let xy_sum_usize = xy_sum_with_carry_out & 0xff;
60					let carry_out_usize = xy_sum_with_carry_out >> 8;
61					let lookup_value = (carry_in_usize << 25)
62						| (carry_out_usize << 24)
63						| (x_in_usize << 16)
64						| (y_in_usize << 8)
65						| xy_sum_usize;
66					lookup_t_u32[lookup_index] = lookup_value as u32;
67				}
68			}
69		}
70	}
71
72	builder.pop_namespace();
73	Ok(lookup_t)
74}
75
76pub fn add_carryfree_lookup(
77	builder: &mut ConstraintSystemBuilder,
78	name: impl ToString + Clone,
79) -> Result<OracleId, anyhow::Error> {
80	builder.push_namespace(name);
81
82	let lookup_t = builder.add_committed("lookup_t", T_LOG_SIZE_ADD, B32::TOWER_LEVEL);
83
84	if let Some(witness) = builder.witness() {
85		let mut lookup_t = witness.new_column::<B32>(lookup_t);
86
87		let lookup_t_u32 = lookup_t.as_mut_slice::<u32>();
88
89		for carry_in_usize in 0..(1 << 1) {
90			for x_in_usize in 0..(1 << 8) {
91				for y_in_usize in 0..(1 << 8) {
92					let lookup_index = (carry_in_usize << 16) | (x_in_usize << 8) | y_in_usize;
93					let xy_sum_usize = x_in_usize + y_in_usize + carry_in_usize;
94
95					// Make it impossible to add numbers resulting in a carry
96					let lookup_value = if xy_sum_usize <= 0xff {
97						(carry_in_usize << 24)
98							| (x_in_usize << 16) | (y_in_usize << 8)
99							| xy_sum_usize
100					} else {
101						0
102					};
103					lookup_t_u32[lookup_index] = lookup_value as u32;
104				}
105			}
106		}
107	}
108
109	builder.pop_namespace();
110	Ok(lookup_t)
111}
112
113pub fn dci_lookup(
114	builder: &mut ConstraintSystemBuilder,
115	name: impl ToString + Clone,
116) -> Result<OracleId, anyhow::Error> {
117	builder.push_namespace(name);
118
119	let lookup_t = builder.add_committed("lookup_t", T_LOG_SIZE_DCI, B32::TOWER_LEVEL);
120
121	if let Some(witness) = builder.witness() {
122		let mut lookup_t = witness.new_column::<B32>(lookup_t);
123
124		let lookup_t_u32 = lookup_t.as_mut_slice::<u32>();
125
126		for first_carry_in_usize in 0..(1 << 1) {
127			for second_carry_in_usize in 0..(1 << 1) {
128				for x_in_usize in 0..(1 << 8) {
129					let lookup_index =
130						(first_carry_in_usize << 9) | (second_carry_in_usize << 8) | x_in_usize;
131					let sum_with_carry_out =
132						x_in_usize + first_carry_in_usize + second_carry_in_usize;
133					let sum_usize = sum_with_carry_out & 0xff;
134					let carry_out_usize = sum_with_carry_out >> 8;
135					let lookup_value = (first_carry_in_usize << 18)
136						| (second_carry_in_usize << 17)
137						| (carry_out_usize << 16)
138						| (x_in_usize << 8)
139						| sum_usize;
140					lookup_t_u32[lookup_index] = lookup_value as u32;
141				}
142			}
143		}
144	}
145
146	builder.pop_namespace();
147	Ok(lookup_t)
148}
149
150#[cfg(test)]
151mod tests {
152	use binius_field::{BinaryField1b, BinaryField32b, BinaryField8b};
153
154	use crate::{
155		builder::test_utils::test_circuit,
156		lasso::{self, batch::LookupBatch},
157		unconstrained::unconstrained,
158	};
159
160	#[test]
161	fn test_lasso_u8add_carryfree_rejects_carry() {
162		// TODO: Make this test 100% certain to pass instead of 2^14 bits of security from
163		// randomness
164		test_circuit(|builder| {
165			let log_size = 14;
166			let x_in = unconstrained::<BinaryField8b>(builder, "x", log_size)?;
167			let y_in = unconstrained::<BinaryField8b>(builder, "y", log_size)?;
168			let c_in = unconstrained::<BinaryField1b>(builder, "c", log_size)?;
169
170			let lookup_t = super::add_carryfree_lookup(builder, "add cf table")?;
171			let mut lookup_batch = LookupBatch::new([lookup_t]);
172			let _sum_and_cout = lasso::u8add_carryfree(
173				builder,
174				&mut lookup_batch,
175				"lasso_u8add",
176				x_in,
177				y_in,
178				c_in,
179				log_size,
180			)?;
181			lookup_batch.execute::<BinaryField32b>(builder)?;
182			Ok(vec![])
183		})
184		.expect_err("Rejected overflowing add");
185	}
186
187	#[test]
188	fn test_lasso_u8mul() {
189		test_circuit(|builder| {
190			let log_size = 10;
191
192			let mult_a = unconstrained::<BinaryField8b>(builder, "mult_a", log_size)?;
193			let mult_b = unconstrained::<BinaryField8b>(builder, "mult_b", log_size)?;
194
195			let mul_lookup_table = super::mul_lookup(builder, "mul table")?;
196
197			let mut lookup_batch = LookupBatch::new([mul_lookup_table]);
198
199			let _product = lasso::u8mul(
200				builder,
201				&mut lookup_batch,
202				"lasso_u8mul",
203				mult_a,
204				mult_b,
205				1 << log_size,
206			)?;
207
208			lookup_batch.execute::<BinaryField32b>(builder)?;
209			Ok(vec![])
210		})
211		.unwrap();
212	}
213
214	#[test]
215	fn test_lasso_batched_u8mul() {
216		test_circuit(|builder| {
217			let log_size = 10;
218			let mul_lookup_table = super::mul_lookup(builder, "mul table")?;
219
220			let mut lookup_batch = LookupBatch::new([mul_lookup_table]);
221
222			for _ in 0..10 {
223				let mult_a = unconstrained::<BinaryField8b>(builder, "mult_a", log_size)?;
224				let mult_b = unconstrained::<BinaryField8b>(builder, "mult_b", log_size)?;
225
226				let _product = lasso::u8mul(
227					builder,
228					&mut lookup_batch,
229					"lasso_u8mul",
230					mult_a,
231					mult_b,
232					1 << log_size,
233				)?;
234			}
235
236			lookup_batch.execute::<BinaryField32b>(builder)?;
237			Ok(vec![])
238		})
239		.unwrap();
240	}
241
242	#[test]
243	fn test_lasso_batched_u8mul_rejects() {
244		test_circuit(|builder| {
245			let log_size = 10;
246
247			// We try to feed in the add table instead
248			let mul_lookup_table = super::add_lookup(builder, "mul table")?;
249
250			let mut lookup_batch = LookupBatch::new([mul_lookup_table]);
251
252			// TODO?: Make this test fail 100% of the time, even though its almost impossible with
253			// rng
254			for _ in 0..10 {
255				let mult_a = unconstrained::<BinaryField8b>(builder, "mult_a", log_size)?;
256				let mult_b = unconstrained::<BinaryField8b>(builder, "mult_b", log_size)?;
257				let _product = lasso::u8mul(
258					builder,
259					&mut lookup_batch,
260					"lasso_u8mul",
261					mult_a,
262					mult_b,
263					1 << log_size,
264				)?;
265			}
266
267			lookup_batch.execute::<BinaryField32b>(builder)?;
268			Ok(vec![])
269		})
270		.expect_err("Channels should be unbalanced");
271	}
272}