boo_hoo/
program.rs

1use bincode::{enc::Encoder, error::EncodeError, Encode};
2use std::{error, fmt};
3
4use crate::bits::{Bit, BitBuf};
5
6/// Represents an individual operation in the program.
7///
8/// Each of these manipulates the program stack, potentially reading input.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum Operation {
11    /// PUSH(!POP)
12    Not,
13    /// PUSH(POP & POP)
14    And,
15    /// PUSH(POP ^ POP)
16    Xor,
17    /// Push an argument bit, with the right index.
18    PushArg(u32),
19    /// Push a local element, indexed from the bottom of the stack
20    PushLocal(u32),
21    /// Pop an element, moving it to the output
22    PopOutput,
23}
24
25impl Encode for Operation {
26    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
27        match self {
28            Operation::Not => 0u8.encode(encoder),
29            Operation::And => 1u8.encode(encoder),
30            Operation::Xor => 2u8.encode(encoder),
31            Operation::PushArg(index) => {
32                3u8.encode(encoder)?;
33                index.encode(encoder)
34            }
35            Operation::PushLocal(index) => {
36                4u8.encode(encoder)?;
37                index.encode(encoder)
38            }
39            Operation::PopOutput => 5u8.encode(encoder),
40        }
41    }
42}
43
44/// An error that describes an invalid program.
45#[derive(Clone, Debug, PartialEq)]
46pub enum ProgramError {
47    /// The program had an insufficient stack at a given instruction.
48    InsufficientStack { instruction: usize, stack: usize },
49}
50
51impl fmt::Display for ProgramError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            ProgramError::InsufficientStack { instruction, stack } => {
55                write!(
56                    f,
57                    "instr {}: insufficient stack of size {}",
58                    instruction, stack
59                )
60            }
61        }
62    }
63}
64
65impl error::Error for ProgramError {}
66
67/// Represents a program, implementing some kind of boolean circuit.
68///
69/// The programs represent boolean circuits using a sequence of stack-based
70/// operations.
71#[derive(Clone, Debug, PartialEq)]
72pub struct Program {
73    operations: Vec<Operation>,
74}
75
76impl Program {
77    /// Create a program from a list of operations.
78    ///
79    /// The program will execute the operations in order.
80    pub fn new(operations: impl Into<Vec<Operation>>) -> Self {
81        Self {
82            operations: operations.into(),
83        }
84    }
85
86    /// Validate that a program is well formed.
87    ///
88    /// A program isn't well formed if it attempts to pop an element off of an
89    /// empty stack, or access some other undefined element.
90    ///
91    /// We produce a new struct to allow for reusing the validated result
92    /// without redoing the logic.
93    pub fn validate(self) -> Result<ValidatedProgram, ProgramError> {
94        let mut required_input: u32 = 0;
95        let mut output_count: usize = 0;
96        let mut stack: usize = 0;
97
98        for (instruction, op) in self.operations.iter().enumerate() {
99            use Operation::*;
100
101            match op {
102                Not => {
103                    if stack < 1 {
104                        return Err(ProgramError::InsufficientStack { instruction, stack });
105                    }
106                }
107                Xor | And => {
108                    if stack < 2 {
109                        return Err(ProgramError::InsufficientStack { instruction, stack });
110                    }
111                    stack -= 1;
112                }
113                Operation::PushArg(i) => {
114                    required_input = u32::max(i + 1, required_input);
115                    stack += 1;
116                }
117                Operation::PushLocal(i) => {
118                    if (*i as usize) >= stack {
119                        return Err(ProgramError::InsufficientStack { instruction, stack });
120                    }
121                    stack += 1;
122                }
123                Operation::PopOutput => {
124                    if stack < 1 {
125                        return Err(ProgramError::InsufficientStack { instruction, stack });
126                    }
127                    stack -= 1;
128                    output_count += 1;
129                }
130            }
131        }
132
133        Ok(ValidatedProgram {
134            input_count: required_input as usize,
135            output_count,
136            operations: self.operations,
137        })
138    }
139}
140
141/// Represents a valid program.
142///
143/// Unlike a general program, we make sure that undefined elements of the stack
144/// are never accessed. We also already know the number of input bits required,
145/// as well as the number of output bits produced.
146///
147/// By having this information in a struct, we avoid re-validating a program
148/// if it's used multiple times.
149#[derive(Clone, Debug)]
150pub struct ValidatedProgram {
151    pub(crate) input_count: usize,
152    pub(crate) output_count: usize,
153    pub(crate) operations: Vec<Operation>,
154}
155
156// Utility functions for testing
157#[cfg(test)]
158impl ValidatedProgram {
159    fn io(&self) -> (usize, usize) {
160        (self.input_count, self.output_count)
161    }
162}
163
164/// Represents a stack machine capable of interpreting our bytecode.
165///
166/// This is useful in simulating programs for testing purposes, but also for doing
167/// simulation of individual parties inside of our proving system as well.
168#[derive(Clone, Debug)]
169pub(crate) struct Machine {
170    input: BitBuf,
171    stack: BitBuf,
172    output: BitBuf,
173}
174
175impl Machine {
176    /// Create a new machine with a given input buffer.
177    pub fn new(input: BitBuf) -> Self {
178        Self {
179            input,
180            stack: BitBuf::new(),
181            output: BitBuf::new(),
182        }
183    }
184
185    /// Pop a bit off the stack.
186    ///
187    /// This is UB of the stack is empty. This is why validating the program
188    /// being run is important.
189    pub fn pop(&mut self) -> Bit {
190        self.stack.pop().unwrap()
191    }
192
193    /// Push a bit onto the stack.
194    pub fn push(&mut self, bit: Bit) {
195        self.stack.push(bit);
196    }
197
198    /// Negate the top bit on the stack.
199    pub fn not(&mut self) {
200        let top = self.pop();
201        self.push(!top);
202    }
203
204    /// xor the top two bits off the stack.
205    pub fn xor(&mut self) {
206        let a = self.pop();
207        let b = self.pop();
208        self.push(a ^ b);
209    }
210
211    /// and the top two bits off the stack.
212    ///
213    /// Note that this operation doesn't do the correct thing in the secret sharing
214    /// setting. This is only needed for tests as well
215    #[cfg(test)]
216    pub fn and(&mut self) {
217        let a = self.pop();
218        let b = self.pop();
219        self.push(a & b);
220    }
221
222    /// Push a bit of the input onto the stack.
223    pub fn push_arg(&mut self, i: usize) {
224        let arg = self.input.get(i).unwrap();
225        self.push(arg);
226    }
227
228    /// Push a bit from the stack back onto the stack.
229    pub fn push_local(&mut self, i: usize) {
230        let local = self.stack.get(i).unwrap();
231        self.push(local);
232    }
233
234    /// Pop a top bit and move it to the output buffer.
235    pub fn pop_output(&mut self) {
236        let pop = self.pop();
237        self.output.push(pop)
238    }
239
240    /// Consume this machine, returning the input and output buffers.
241    pub fn input_output(self) -> (BitBuf, BitBuf) {
242        (self.input, self.output)
243    }
244}
245
246/// A module providing generators for property testing
247#[cfg(test)]
248pub(crate) mod generators {
249    use super::*;
250    use proptest::collection::vec;
251    use proptest::prelude::*;
252
253    fn run_operations(ops: &[Operation], input: &[u8]) -> u8 {
254        let mut machine = Machine::new(BitBuf::from_bytes(input));
255        for op in ops {
256            match op {
257                Operation::Not => machine.not(),
258                Operation::And => machine.and(),
259                Operation::Xor => machine.xor(),
260                Operation::PushArg(i) => machine.push_arg(*i as usize),
261                Operation::PushLocal(i) => machine.push_local(*i as usize),
262                Operation::PopOutput => machine.pop_output(),
263            }
264        }
265        let (_, mut output) = machine.input_output();
266        let mut out = 0;
267        for _ in 0..8 {
268            out <<= 1;
269            out |= u64::from(output.pop().unwrap()) as u8;
270        }
271        out
272    }
273
274    fn arb_operation_except_pop_output(
275        max_arg: u32,
276        max_local: u32,
277    ) -> impl Strategy<Value = Operation> {
278        use Operation::*;
279
280        prop_oneof![
281            Just(Not),
282            Just(And),
283            Just(Xor),
284            (0..max_arg).prop_map(PushArg),
285            (0..max_local).prop_map(PushLocal),
286        ]
287    }
288
289    // The idea is to have 32 bits of input pushed to the stack, and only do 16
290    // operations, before then popping 8 bits of input. This ensure no stack underflow
291    // can happen, and that we'll have enough output.
292    prop_compose! {
293        pub fn arb_program_and_inputs()(input in any::<[u8; 4]>(), extra in vec(arb_operation_except_pop_output(32, 16), 0..16)) -> (ValidatedProgram, [u8; 4], u8) {
294            use Operation::*;
295
296            let mut operations = Vec::with_capacity(extra.len() + 32 + 8);
297            for i in 0..32 {
298                operations.push(PushArg(i));
299            }
300            operations.extend(extra.iter());
301            for _ in 0..8 {
302                operations.push(PopOutput);
303            }
304
305            let output = run_operations(&operations, &input);
306            (Program::new(operations).validate().unwrap(), input, output)
307        }
308    }
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314    use Operation::*;
315
316    #[test]
317    fn test_validating_program_with_insufficient_stack() {
318        assert!(Program::new([Not]).validate().is_err());
319        assert!(Program::new([And]).validate().is_err());
320        assert!(Program::new([PushArg(0), And]).validate().is_err());
321        assert!(Program::new([PushLocal(0)]).validate().is_err());
322    }
323
324    #[test]
325    fn test_validating_program_counts_correctly() {
326        assert_eq!(
327            Ok((2, 2)),
328            Program::new([PushArg(0), PushArg(1), Not, PopOutput, PopOutput])
329                .validate()
330                .map(|x| x.io())
331        );
332        assert_eq!(
333            Ok((1, 2)),
334            Program::new([PushArg(0), PushArg(0), Not, PopOutput, PopOutput])
335                .validate()
336                .map(|x| x.io())
337        );
338        assert_eq!(
339            Ok((1, 1)),
340            Program::new([PushArg(0), PushArg(0), Xor, PopOutput])
341                .validate()
342                .map(|x| x.io())
343        );
344        assert_eq!(
345            Ok((1, 1)),
346            Program::new([PushArg(0), PushArg(0), And, PopOutput])
347                .validate()
348                .map(|x| x.io())
349        );
350    }
351}