Skip to content

Commit 449ef65

Browse files
authored
Merge pull request #113 from sine-fdn/v0.2
Hide low-level type details when using `compile` + `GarbleProgram`
2 parents 223243d + 364e88a commit 449ef65

9 files changed

+493
-297
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "garble_lang"
3-
version = "0.1.8"
3+
version = "0.2.0"
44
edition = "2021"
55
rust-version = "1.60.0"
66
description = "Turing-Incomplete Programming Language for Multi-Party Computation with Garbled Circuits"

src/circuit.rs

+105-2
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,25 @@
33
use crate::{compile::wires_as_unsigned, env::Env, token::MetaInfo};
44
use std::collections::HashMap;
55

6+
#[cfg(feature = "serde")]
7+
use serde::{Deserialize, Serialize};
8+
69
// This module currently implements a few basic kinds of circuit optimizations:
710
//
811
// 1. Constant evaluation (e.g. x ^ 0 == x; x & 1 == x; x & 0 == 0)
912
// 2. Sub-expression sharing (wires are re-used if a gate with the same type and inputs exists)
1013
// 3. Pruning of useless gates (gates that are not part of the output nor used by other gates)
1114

1215
const PRINT_OPTIMIZATION_RATIO: bool = false;
16+
const MAX_GATES: usize = (u32::MAX >> 4) as usize;
17+
const MAX_AND_GATES: usize = (u32::MAX >> 8) as usize;
1318

1419
/// Data type to uniquely identify gates.
1520
pub type GateIndex = usize;
1621

17-
/// Description of a gate executed under S-MPC.
22+
/// Description of a gate executed under MPC.
1823
#[derive(Debug, Clone, PartialEq, Eq)]
24+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1925
pub enum Gate {
2026
/// A logical XOR gate attached to the two specified input wires.
2127
Xor(GateIndex, GateIndex),
@@ -25,7 +31,7 @@ pub enum Gate {
2531
Not(GateIndex),
2632
}
2733

28-
/// Representation of a circuit evaluated by an S-MPC engine.
34+
/// Representation of a circuit evaluated by an MPC engine.
2935
///
3036
/// Each circuit consists of 3 parts:
3137
///
@@ -66,6 +72,7 @@ pub enum Gate {
6672
/// true and constant false, specified as `Gate::Xor(0, 0)` with wire `n` and `Gate::Not(n)` (and
6773
/// thus depend on the first input bit for their specifications).
6874
#[derive(Debug, Clone)]
75+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6976
pub struct Circuit {
7077
/// The different parties, with `usize` at index `i` as the number of input bits for party `i`.
7178
pub input_gates: Vec<usize>,
@@ -75,7 +82,103 @@ pub struct Circuit {
7582
pub output_gates: Vec<GateIndex>,
7683
}
7784

85+
/// An input wire or a gate operating on them.
86+
pub enum Wire {
87+
/// An input wire, with its value coming directly from one of the parties.
88+
Input(GateIndex),
89+
/// A logical XOR gate attached to the two specified input wires.
90+
Xor(GateIndex, GateIndex),
91+
/// A logical AND gate attached to the two specified input wires.
92+
And(GateIndex, GateIndex),
93+
/// A logical NOT gate attached to the specified input wire.
94+
Not(GateIndex),
95+
}
96+
97+
/// Errors occurring during the validation or the execution of the MPC protocol.
98+
#[derive(Debug, PartialEq, Eq)]
99+
pub enum CircuitError {
100+
/// The gate with the specified wire contains invalid gate connections.
101+
InvalidGate(usize),
102+
/// The specified output gate does not exist in the circuit.
103+
InvalidOutput(usize),
104+
/// The circuit does not specify any output gates.
105+
EmptyOutputs,
106+
/// The provided circuit has too many gates to be processed.
107+
MaxCircuitSizeExceeded,
108+
/// The provided index does not correspond to any party.
109+
PartyIndexOutOfBounds,
110+
}
111+
78112
impl Circuit {
113+
/// Returns all the wires (inputs + gates) in the circuit, in ascending order.
114+
pub fn wires(&self) -> Vec<Wire> {
115+
let mut gates = vec![];
116+
for (party, inputs) in self.input_gates.iter().enumerate() {
117+
for _ in 0..*inputs {
118+
gates.push(Wire::Input(party))
119+
}
120+
}
121+
for gate in self.gates.iter() {
122+
let gate = match gate {
123+
Gate::Xor(x, y) => Wire::Xor(*x, *y),
124+
Gate::And(x, y) => Wire::And(*x, *y),
125+
Gate::Not(x) => Wire::Not(*x),
126+
};
127+
gates.push(gate);
128+
}
129+
gates
130+
}
131+
132+
/// Returns the number of AND gates in the circuit.
133+
pub fn and_gates(&self) -> usize {
134+
self.gates
135+
.iter()
136+
.filter(|g| matches!(g, Gate::And(_, _)))
137+
.count()
138+
}
139+
140+
/// Checks that the circuit only uses valid wires, includes no cycles, has outputs, etc.
141+
pub fn validate(&self) -> Result<(), CircuitError> {
142+
let mut num_and_gates = 0;
143+
let wires = self.wires();
144+
for (i, g) in wires.iter().enumerate() {
145+
match g {
146+
Wire::Input(_) => {}
147+
&Wire::Xor(x, y) => {
148+
if x >= i || y >= i {
149+
return Err(CircuitError::InvalidGate(i));
150+
}
151+
}
152+
&Wire::And(x, y) => {
153+
if x >= i || y >= i {
154+
return Err(CircuitError::InvalidGate(i));
155+
}
156+
num_and_gates += 1;
157+
}
158+
&Wire::Not(x) => {
159+
if x >= i {
160+
return Err(CircuitError::InvalidGate(i));
161+
}
162+
}
163+
}
164+
}
165+
if self.output_gates.is_empty() {
166+
return Err(CircuitError::EmptyOutputs);
167+
}
168+
for &o in self.output_gates.iter() {
169+
if o >= wires.len() {
170+
return Err(CircuitError::InvalidOutput(o));
171+
}
172+
}
173+
if num_and_gates > MAX_AND_GATES {
174+
return Err(CircuitError::MaxCircuitSizeExceeded);
175+
}
176+
if wires.len() > MAX_GATES {
177+
return Err(CircuitError::MaxCircuitSizeExceeded);
178+
}
179+
Ok(())
180+
}
181+
79182
/// Evaluates the circuit with the specified inputs (with one `Vec<bool>` per party).
80183
///
81184
/// Assumes that the inputs have been previously type-checked and **panics** if the number of

src/eval.rs

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub enum EvalError {
4343
UnexpectedNumberOfInputsFromParty(usize),
4444
/// An input literal could not be parsed.
4545
LiteralParseError(CompileTimeError),
46+
/// The circuit does not have an input argument with the given index.
47+
InvalidArgIndex(usize),
4648
/// The literal is not of the expected parameter type.
4749
InvalidLiteralType(Literal, Type),
4850
/// The number of output bits does not match the expected type.
@@ -68,6 +70,9 @@ impl std::fmt::Display for EvalError {
6870
EvalError::LiteralParseError(err) => {
6971
err.fmt(f)
7072
}
73+
EvalError::InvalidArgIndex(i) => {
74+
f.write_fmt(format_args!("The circuit does not an input argument with index {i}"))
75+
}
7176
EvalError::InvalidLiteralType(literal, ty) => {
7277
f.write_fmt(format_args!("The argument literal is not of type {ty}: '{literal}'"))
7378
}

src/lib.rs

+127-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,59 @@
11
//! A purely functional programming language with a Rust-like syntax that compiles to logic gates
22
//! for secure multi-party computation.
3+
//!
4+
//! Garble programs always terminate and are compiled into a combination of boolean AND / XOR / NOT
5+
//! gates. These boolean circuits can either be executed directly (mostly for testing purposes) or
6+
//! passed to a multi-party computation engine.
7+
//!
8+
//! ```rust
9+
//! use garble_lang::{compile, literal::Literal, token::UnsignedNumType::U32};
10+
//!
11+
//! // Compile and type-check a simple program to add the inputs of 3 parties:
12+
//! let code = "pub fn main(x: u32, y: u32, z: u32) -> u32 { x + y + z }";
13+
//! let prg = compile(code).map_err(|e| e.prettify(&code)).unwrap();
14+
//!
15+
//! // We can evaluate the circuit directly, useful for testing purposes:
16+
//! let mut eval = prg.evaluator();
17+
//! eval.set_u32(2);
18+
//! eval.set_u32(10);
19+
//! eval.set_u32(100);
20+
//! let output = eval.run().map_err(|e| e.prettify(&code)).unwrap();
21+
//! assert_eq!(u32::try_from(output).map_err(|e| e.prettify(&code)).unwrap(), 2 + 10 + 100);
22+
//!
23+
//! // Or we can run the compiled circuit in an MPC engine, simulated using `prg.circuit.eval()`:
24+
//! let x = prg.parse_arg(0, "2u32").unwrap().as_bits();
25+
//! let y = prg.parse_arg(1, "10u32").unwrap().as_bits();
26+
//! let z = prg.parse_arg(2, "100u32").unwrap().as_bits();
27+
//! let output = prg.circuit.eval(&[x, y, z]); // use your own MPC engine here instead
28+
//! let result = prg.parse_output(&output).unwrap();
29+
//! assert_eq!("112u32", result.to_string());
30+
//!
31+
//! // Input arguments can also be constructed directly as literals:
32+
//! let x = prg.literal_arg(0, Literal::NumUnsigned(2, U32)).unwrap().as_bits();
33+
//! let y = prg.literal_arg(1, Literal::NumUnsigned(10, U32)).unwrap().as_bits();
34+
//! let z = prg.literal_arg(2, Literal::NumUnsigned(100, U32)).unwrap().as_bits();
35+
//! let output = prg.circuit.eval(&[x, y, z]); // use your own MPC engine here instead
36+
//! let result = prg.parse_output(&output).unwrap();
37+
//! assert_eq!(Literal::NumUnsigned(112, U32), result);
38+
//! ```
339
440
#![deny(unsafe_code)]
541
#![deny(missing_docs)]
642
#![deny(rustdoc::broken_intra_doc_links)]
743

44+
use ast::{Expr, FnDef, Pattern, Program, Stmt, Type, VariantExpr};
845
use check::TypeError;
46+
use circuit::Circuit;
947
use compile::CompilerError;
10-
use eval::EvalError;
48+
use eval::{EvalError, Evaluator};
49+
use literal::Literal;
1150
use parse::ParseError;
1251
use scan::{scan, ScanError};
13-
use std::fmt::Write as _;
52+
use std::fmt::{Display, Write as _};
1453
use token::MetaInfo;
1554

16-
use ast::{Expr, FnDef, Pattern, Program, Stmt, Type, VariantExpr};
17-
use circuit::Circuit;
55+
#[cfg(feature = "serde")]
56+
use serde::{Deserialize, Serialize};
1857

1958
/// [`crate::ast::Program`] without any associated type information.
2059
pub type UntypedProgram = Program<()>;
@@ -56,12 +95,91 @@ pub fn check(prg: &str) -> Result<TypedProgram, Error> {
5695
Ok(scan(prg)?.parse()?.type_check()?)
5796
}
5897

59-
/// Scans, parses, type-checks and then compiles a program to a circuit of gates.
60-
pub fn compile(prg: &str, fn_name: &str) -> Result<(TypedProgram, TypedFnDef, Circuit), Error> {
98+
/// Scans, parses, type-checks and then compiles the `"main"` fn of a program to a boolean circuit.
99+
pub fn compile(prg: &str) -> Result<GarbleProgram, Error> {
61100
let program = check(prg)?;
62-
let (circuit, main_fn) = program.compile(fn_name)?;
63-
let main_fn = main_fn.clone();
64-
Ok((program, main_fn, circuit))
101+
let (circuit, main) = program.compile("main")?;
102+
let main = main.clone();
103+
Ok(GarbleProgram {
104+
program,
105+
main,
106+
circuit,
107+
})
108+
}
109+
110+
/// The result of type-checking and compiling a Garble program.
111+
#[derive(Debug, Clone)]
112+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113+
pub struct GarbleProgram {
114+
/// The type-checked represenation of the full program.
115+
pub program: TypedProgram,
116+
/// The function to be executed as a circuit.
117+
pub main: TypedFnDef,
118+
/// The compilation output, as a circuit of boolean gates.
119+
pub circuit: Circuit,
120+
}
121+
122+
/// An input argument for a Garble program and circuit.
123+
#[derive(Debug, Clone)]
124+
pub struct GarbleArgument<'a>(Literal, &'a TypedProgram);
125+
126+
impl GarbleProgram {
127+
/// Returns an evaluator that can be used to run the compiled circuit.
128+
pub fn evaluator(&self) -> Evaluator<'_> {
129+
Evaluator::new(&self.program, &self.main, &self.circuit)
130+
}
131+
132+
/// Type-checks and uses the literal as the circuit input argument with the given index.
133+
pub fn literal_arg(
134+
&self,
135+
arg_index: usize,
136+
literal: Literal,
137+
) -> Result<GarbleArgument<'_>, EvalError> {
138+
let Some(param) = self.main.params.get(arg_index) else {
139+
return Err(EvalError::InvalidArgIndex(arg_index));
140+
};
141+
if !literal.is_of_type(&self.program, &param.ty) {
142+
return Err(EvalError::InvalidLiteralType(literal, param.ty.clone()));
143+
}
144+
Ok(GarbleArgument(literal, &self.program))
145+
}
146+
147+
/// Tries to parse the string as the circuit input argument with the given index.
148+
pub fn parse_arg(
149+
&self,
150+
arg_index: usize,
151+
literal: &str,
152+
) -> Result<GarbleArgument<'_>, EvalError> {
153+
let Some(param) = self.main.params.get(arg_index) else {
154+
return Err(EvalError::InvalidArgIndex(arg_index));
155+
};
156+
let literal = Literal::parse(&self.program, &param.ty, literal)
157+
.map_err(EvalError::LiteralParseError)?;
158+
Ok(GarbleArgument(literal, &self.program))
159+
}
160+
161+
/// Tries to convert the circuit output back to a Garble literal.
162+
pub fn parse_output(&self, bits: &[bool]) -> Result<Literal, EvalError> {
163+
Literal::from_result_bits(&self.program, &self.main.ty, bits)
164+
}
165+
}
166+
167+
impl GarbleArgument<'_> {
168+
/// Converts the argument to input bits for the compiled circuit.
169+
pub fn as_bits(&self) -> Vec<bool> {
170+
self.0.as_bits(self.1)
171+
}
172+
173+
/// Converts the argument to a Garble literal.
174+
pub fn as_literal(&self) -> Literal {
175+
self.0.clone()
176+
}
177+
}
178+
179+
impl Display for GarbleArgument<'_> {
180+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181+
self.0.fmt(f)
182+
}
65183
}
66184

67185
/// Errors that can occur during compile time, while a program is scanned, parsed or type-checked.

0 commit comments

Comments
 (0)