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 randomness
163		test_circuit(|builder| {
164			let log_size = 14;
165			let x_in = unconstrained::<BinaryField8b>(builder, "x", log_size)?;
166			let y_in = unconstrained::<BinaryField8b>(builder, "y", log_size)?;
167			let c_in = unconstrained::<BinaryField1b>(builder, "c", log_size)?;
168
169			let lookup_t = super::add_carryfree_lookup(builder, "add cf table")?;
170			let mut lookup_batch = LookupBatch::new([lookup_t]);
171			let _sum_and_cout = lasso::u8add_carryfree(
172				builder,
173				&mut lookup_batch,
174				"lasso_u8add",
175				x_in,
176				y_in,
177				c_in,
178				log_size,
179			)?;
180			lookup_batch.execute::<BinaryField32b>(builder)?;
181			Ok(vec![])
182		})
183		.expect_err("Rejected overflowing add");
184	}
185
186	#[test]
187	fn test_lasso_u8mul() {
188		test_circuit(|builder| {
189			let log_size = 10;
190
191			let mult_a = unconstrained::<BinaryField8b>(builder, "mult_a", log_size)?;
192			let mult_b = unconstrained::<BinaryField8b>(builder, "mult_b", log_size)?;
193
194			let mul_lookup_table = super::mul_lookup(builder, "mul table")?;
195
196			let mut lookup_batch = LookupBatch::new([mul_lookup_table]);
197
198			let _product = lasso::u8mul(
199				builder,
200				&mut lookup_batch,
201				"lasso_u8mul",
202				mult_a,
203				mult_b,
204				1 << log_size,
205			)?;
206
207			lookup_batch.execute::<BinaryField32b>(builder)?;
208			Ok(vec![])
209		})
210		.unwrap();
211	}
212
213	#[test]
214	fn test_lasso_batched_u8mul() {
215		test_circuit(|builder| {
216			let log_size = 10;
217			let mul_lookup_table = super::mul_lookup(builder, "mul table")?;
218
219			let mut lookup_batch = LookupBatch::new([mul_lookup_table]);
220
221			for _ in 0..10 {
222				let mult_a = unconstrained::<BinaryField8b>(builder, "mult_a", log_size)?;
223				let mult_b = unconstrained::<BinaryField8b>(builder, "mult_b", log_size)?;
224
225				let _product = lasso::u8mul(
226					builder,
227					&mut lookup_batch,
228					"lasso_u8mul",
229					mult_a,
230					mult_b,
231					1 << log_size,
232				)?;
233			}
234
235			lookup_batch.execute::<BinaryField32b>(builder)?;
236			Ok(vec![])
237		})
238		.unwrap();
239	}
240
241	#[test]
242	fn test_lasso_batched_u8mul_rejects() {
243		test_circuit(|builder| {
244			let log_size = 10;
245
246			// We try to feed in the add table instead
247			let mul_lookup_table = super::add_lookup(builder, "mul table")?;
248
249			let mut lookup_batch = LookupBatch::new([mul_lookup_table]);
250
251			// TODO?: Make this test fail 100% of the time, even though its almost impossible with rng
252			for _ in 0..10 {
253				let mult_a = unconstrained::<BinaryField8b>(builder, "mult_a", log_size)?;
254				let mult_b = unconstrained::<BinaryField8b>(builder, "mult_b", log_size)?;
255				let _product = lasso::u8mul(
256					builder,
257					&mut lookup_batch,
258					"lasso_u8mul",
259					mult_a,
260					mult_b,
261					1 << log_size,
262				)?;
263			}
264
265			lookup_batch.execute::<BinaryField32b>(builder)?;
266			Ok(vec![])
267		})
268		.expect_err("Channels should be unbalanced");
269	}
270}