Skip to main content

binius_examples/circuits/
subset_sum.rs

1// Copyright 2025 Irreducible Inc.
2use binius_core::Word;
3use binius_frontend::{CircuitBuilder, Wire, WitnessFiller};
4
5/// "Subset Sum" is the following "knowledge" problem:
6///
7/// - Given a list of non-negative integers and a target value, do you know a sublist which sums
8///   exactly to the target value?
9///
10/// The circuit has the full list and the target value as public inputs, and
11/// the selected sublist as a private input. It checks that that the sublist
12/// sums to the target value.
13pub struct SubsetSum {
14	/// The number of input integers.
15	///
16	/// Should be equal to `values.len()` and `selection.len()`.
17	len: usize,
18	/// The full list of input integers.
19	values: Vec<Wire>,
20	/// The target value that the sublist should sum to.
21	target: Wire,
22	/// The selection of the sublist. Each entry is interpreted as a boolean.
23	selection: Vec<Wire>,
24}
25
26impl SubsetSum {
27	/// This constructs the circuit in `builder` and stores some of the wires in
28	/// a `SubsetSum` struct, which is then returned.
29	/// The wires are stored so that they can be populated with values later.
30	pub fn construct_circuit(builder: &mut CircuitBuilder, len: usize) -> Self {
31		// the list of integers (public)
32		let mut values = Vec::new();
33		for _ in 0..len {
34			values.push(builder.add_inout());
35		}
36
37		// the target value (public)
38		let target = builder.add_inout();
39
40		// bools which represent the selection of the subset (private)
41		let mut selection = Vec::new();
42		for _ in 0..len {
43			selection.push(builder.add_witness());
44		}
45
46		// mask `values` using `selection`
47		let mut values_masked = Vec::new();
48		for i in 0..len {
49			// the most significant bit is used to interpret `selection[i]` as a boolean,
50			// and we create a bitmask out of that which is either all 0s or all 1s
51			let bit_mask = builder.sar(selection[i], 63);
52			let value_masked = builder.band(values[i], bit_mask);
53			values_masked.push(value_masked);
54		}
55
56		// compute sum of `values_masked`
57		let mut sum = builder.add_constant(Word::ZERO);
58		let mut carry = builder.add_constant(Word::ZERO);
59		for i in 0..len {
60			(sum, carry) = builder.iadd_cin_cout(sum, values_masked[i], carry);
61			// check that no overflow occurred
62			builder.assert_false("no overflow", carry);
63		}
64
65		// check that the sum matches the target
66		builder.assert_eq("sum matches target", sum, target);
67
68		Self {
69			len,
70			values,
71			target,
72			selection,
73		}
74	}
75
76	/// This populates the public wires which define the subset sum problem.
77	///
78	/// - `values` is the list of integers available
79	/// - `target` is the target value that a sublist should sum to
80	pub fn populate_problem(&self, filler: &mut WitnessFiller<'_>, values: &[u64], target: u64) {
81		assert_eq!(values.len(), self.len);
82
83		for i in 0..self.len {
84			filler[self.values[i]] = Word(values[i]);
85		}
86
87		filler[self.target] = Word(target);
88	}
89
90	/// This populates the private wires which select a sublist which is claimed
91	/// to sum to the target value.
92	///
93	/// - `selection` should have the same length as the original list, and should contain `true`
94	///   for every number that should be included in the sublist
95	pub fn populate_solution(&self, filler: &mut WitnessFiller<'_>, selection: &[bool]) {
96		assert_eq!(selection.len(), self.len);
97
98		for i in 0..self.len {
99			let word = if selection[i] {
100				Word::ALL_ONE
101			} else {
102				Word::ZERO
103			};
104			filler[self.selection[i]] = word;
105		}
106	}
107}
108
109#[cfg(test)]
110mod tests {
111
112	use super::*;
113
114	/// Checks that it works with a valid solution.
115	#[test]
116	fn test_valid() {
117		// build circuit
118		let mut builder = CircuitBuilder::new();
119		let subset_sum = SubsetSum::construct_circuit(&mut builder, 5);
120		let circuit = builder.build();
121
122		// populate witness
123		let mut filler = circuit.new_witness_filler();
124		subset_sum.populate_problem(&mut filler, &[2, 5, 5, 3, 7], 17);
125		subset_sum.populate_solution(&mut filler, &[false, true, true, false, true]);
126		circuit.populate_wire_witness(&mut filler).unwrap();
127
128		// check
129		let constraint_system = circuit.constraint_system();
130		constraint_system.verify(&filler.into_value_vec()).unwrap();
131	}
132
133	/// Checks that it fails with an invalid solution.
134	#[test]
135	fn test_invalid() {
136		// build circuit
137		let mut builder = CircuitBuilder::new();
138		let subset_sum = SubsetSum::construct_circuit(&mut builder, 5);
139		let circuit = builder.build();
140
141		// populate witness
142		let mut filler = circuit.new_witness_filler();
143		subset_sum.populate_problem(&mut filler, &[2, 5, 5, 3, 7], 17);
144		subset_sum.populate_solution(&mut filler, &[false, true, true, false, false]);
145		circuit.populate_wire_witness(&mut filler).unwrap_err();
146	}
147
148	/// Checks that it fails if the prover tries to use weird selections to select
149	/// only part of a single value.
150	#[test]
151	fn test_weird_booleans() {
152		// build circuit
153		let mut builder = CircuitBuilder::new();
154		let subset_sum = SubsetSum::construct_circuit(&mut builder, 1);
155		let circuit = builder.build();
156
157		// populate witness
158		let mut filler = circuit.new_witness_filler();
159		subset_sum.populate_problem(&mut filler, &[3], 1);
160		// This is trying to be a malicious prover: If the constraints are not assembled
161		// carefully, this could pass because `values ^ selection` sums to target.
162		// In other words, we carefully selected the bits of `selection` so that they
163		// only extract certain bits of `values` if the constraints use a naive AND.
164		filler[subset_sum.selection[0]] = Word(1);
165		circuit.populate_wire_witness(&mut filler).unwrap_err();
166	}
167
168	/// Checks that it fails even if it's a valid solution modulo 2^64.
169	#[test]
170	fn test_overflow() {
171		// build circuit
172		let mut builder = CircuitBuilder::new();
173		let subset_sum = SubsetSum::construct_circuit(&mut builder, 2);
174		let circuit = builder.build();
175
176		// populate witness
177		let mut filler = circuit.new_witness_filler();
178		subset_sum.populate_problem(&mut filler, &[2 << 62, 3 << 62], 1 << 62);
179		// This is a valid solution modulo 2^64, so if the constraints are not assembled
180		// carefully, this could pass.
181		subset_sum.populate_solution(&mut filler, &[true, true]);
182		circuit.populate_wire_witness(&mut filler).unwrap_err();
183	}
184}