Implementing an arithmetic circuit compiler in Rust involves creating a system that can parse a polynomial expression, construct an arithmetic circuit, and evaluate it. Below is a step-by-step guide to implementing a simple arithmetic circuit compiler in Rust.
As a bonus, we are also generating a Solidity Smart Contract as the output, so we can easily deploy a verifier on Ethereum.
Letโs go!
Step 1: Setting Up the Project
First, create a new Rust project:
cargo new arithmetic_circuit cd arithmetic_circuit
Step 2: Define the Circuit Components
Define the basic components of an arithmetic circuit: nodes, gates, and the circuit itself.
src/lib.rs
pub mod circuit; fn main() { // Example usage use circuit::ArithmeticCircuit; let expr = "(x + y) * y"; let circuit = ArithmeticCircuit::from_expression(expr); let result = circuit.evaluate(&[("x", 2), ("y", 3)]); println!("Result: {}", result); // Should print 15 }
src/circuit.rs
use std::collections::HashMap; #[derive(Debug)] pub enum Gate { Input(String), Add(Box<Node>, Box<Node>), Mul(Box<Node>, Box<Node>), Const(i32), } #[derive(Debug)] pub struct Node { gate: Gate, value: Option<i32>, } impl Node { pub fn new(gate: Gate) -> Self { Node { gate, value: None } } pub fn evaluate(&mut self, inputs: &HashMap<String, i32>) -> i32 { if let Some(val) = self.value { return val; } let result = match &self.gate { Gate::Input(name) => *inputs.get(name).unwrap(), Gate::Add(left, right) => left.evaluate(inputs) + right.evaluate(inputs), Gate::Mul(left, right) => left.evaluate(inputs) * right.evaluate(inputs), Gate::Const(val) => *val, }; self.value = Some(result); result } } pub struct ArithmeticCircuit { root: Node, } impl ArithmeticCircuit { pub fn new(root: Node) -> Self { ArithmeticCircuit { root } } pub fn from_expression(expr: &str) -> Self { // For simplicity, this example will manually create the circuit. // You can replace this with a proper expression parser. let x = Node::new(Gate::Input("x".to_string())); let y = Node::new(Gate::Input("y".to_string())); let add = Node::new(Gate::Add(Box::new(x), Box::new(y))); let mul = Node::new(Gate::Mul(Box::new(add), Box::new(Node::new(Gate::Input("y".to_string()))))); ArithmeticCircuit::new(mul) } pub fn evaluate(&mut self, inputs: &[(&str, i32)]) -> i32 { let input_map: HashMap<String, i32> = inputs.iter().cloned().map(|(k, v)| (k.to_string(), v)).collect(); self.root.evaluate(&input_map) } }
Step 3: Parsing the Expression
In a real-world scenario, youโd want to parse the input expression into an arithmetic circuit. However, for simplicity, this example hardcodes the parsing logic. A proper implementation would involve tokenizing the input expression and building the circuit dynamically.
Step 4: Evaluation
The evaluate
function recursively computes the value of the circuit by traversing the DAG from the output node back to the input nodes.
Step 5: Testing the Implementation
To test the implementation, you can run the main
function defined in lib.rs
. It should construct the circuit for the expression (x + y) * y
and evaluate it with the given inputs.
cargo run
Adding Expression Parsing
To add expression parsing to our arithmetic circuit compiler, we can use the nom
crate, a powerful parser generator for Rust.
Step 1: Add Dependencies
First, add nom
to your Cargo.toml
.
[dependencies] nom = "7.1"
Step 2: Implement the Parsing Logic
Modify src/circuit.rs
to include the parsing logic and build the circuit based on the parsed expression.
src/circuit.rs
use nom::{ branch::alt, character::complete::{char, digit1}, combinator::{map, map_res}, multi::fold_many0, sequence::{delimited, pair}, IResult, }; use std::collections::HashMap; use std::str::FromStr; #[derive(Debug, Clone)] pub enum Gate { Input(String), Add(Box<Node>, Box<Node>), Sub(Box<Node>, Box<Node>), Mul(Box<Node>, Box<Node>), Div(Box<Node>, Box<Node>), Const(i32), } #[derive(Debug, Clone)] pub struct Node { gate: Gate, value: Option<i32>, } impl Node { pub fn new(gate: Gate) -> Self { Node { gate, value: None } } pub fn evaluate(&mut self, inputs: &HashMap<String, i32>) -> i32 { if let Some(val) = self.value { return val; } let result = match &mut self.gate { Gate::Input(name) => *inputs.get(name).unwrap(), Gate::Add(left, right) => left.evaluate(inputs) + right.evaluate(inputs), Gate::Sub(left, right) => left.evaluate(inputs) - right.evaluate(inputs), Gate::Mul(left, right) => left.evaluate(inputs) * right.evaluate(inputs), Gate::Div(left, right) => left.evaluate(inputs) / right.evaluate(inputs), Gate::Const(val) => *val, }; self.value = Some(result); result } } pub struct ArithmeticCircuit { root: Node, } impl ArithmeticCircuit { pub fn new(root: Node) -> Self { ArithmeticCircuit { root } } pub fn from_expression(expr: &str) -> Self { let root = Self::parse_expression(expr).expect("Failed to parse expression").1; ArithmeticCircuit::new(root) } fn parse_expression(input: &str) -> IResult<&str, Node> { let (input, init) = Self::parse_term(input)?; fold_many0( pair(alt((char('+'), char('-'))), Self::parse_term), move || init.clone(), |acc, (op, val): (char, Node)| { if op == '+' { Node::new(Gate::Add(Box::new(acc), Box::new(val))) } else { Node::new(Gate::Sub(Box::new(acc), Box::new(val))) } }, )(input) } fn parse_term(input: &str) -> IResult<&str, Node> { let (input, init) = Self::parse_factor(input)?; fold_many0( pair(alt((char('*'), char('/'))), Self::parse_factor), move || init.clone(), |acc, (op, val): (char, Node)| { if op == '*' { Node::new(Gate::Mul(Box::new(acc), Box::new(val))) } else { Node::new(Gate::Div(Box::new(acc), Box::new(val))) } }, )(input) } fn parse_factor(input: &str) -> IResult<&str, Node> { alt(( map_res(digit1, |digit_str: &str| { i32::from_str(digit_str).map(|val| Node::new(Gate::Const(val))) }), map(Self::parse_identifier, |id: &str| { Node::new(Gate::Input(id.to_string())) }), delimited( char('('), Self::parse_expression, char(')'), ), ))(input) } fn parse_identifier(input: &str) -> IResult<&str, &str> { nom::character::complete::alpha1(input) } pub fn evaluate(&mut self, inputs: &[(&str, i32)]) -> i32 { let input_map: HashMap<String, i32> = inputs.iter().cloned().map(|(k, v)| (k.to_string(), v)).collect(); self.root.evaluate(&input_map) } }
Step 3: Update the Main Function
Update the main
function to test the expression parsing and circuit evaluation.
src/main.rs
use arithmetic_circuit::circuit::ArithmeticCircuit; fn main() { let expr = "(x + y) * y"; let mut circuit = ArithmeticCircuit::from_expression(expr); let result = circuit.evaluate(&[("x", 2), ("y", 3)]); println!("Result: {}", result); // Should print 15 }
Step 4: Run the Project
Run the project to see the results.
cargo run
This should output Result: 15
.
Generating a Solidity Smart Contract that can verify arithmetic circuit proofs
To generate a Solidity smart contract that can verify arithmetic circuit proofs, weโll need to translate the arithmetic circuit into Solidity code. This involves generating a contract that can take inputs and compute the output based on the circuit.
Letโs enhance our Rust program to generate Solidity code for the arithmetic circuit.
Step 1: Enhance the Rust Program
First, weโll update the Rust program to generate Solidity code based on the parsed arithmetic circuit.
src/circuit.rs
Add a method to generate Solidity code for the arithmetic circuit:
use nom::{ branch::alt, character::complete::{char, digit1}, combinator::{map, map_res}, multi::fold_many0, sequence::{delimited, pair}, IResult, }; use std::collections::HashMap; use std::str::FromStr; #[derive(Debug)] pub enum Gate { Input(String), Add(Box<Node>, Box<Node>), Sub(Box<Node>, Box<Node>), Mul(Box<Node>, Box<Node>), Div(Box<Node>, Box<Node>), Const(i32), } #[derive(Debug)] pub struct Node { gate: Gate, value: Option<i32>, } impl Node { pub fn new(gate: Gate) -> Self { Node { gate, value: None } } pub fn evaluate(&mut self, inputs: &HashMap<String, i32>) -> i32 { if let Some(val) = self.value { return val; } let result = match &mut self.gate { Gate::Input(name) => *inputs.get(name).unwrap(), Gate::Add(left, right) => left.evaluate(inputs) + right.evaluate(inputs), Gate::Sub(left, right) => left.evaluate(inputs) - right.evaluate(inputs), Gate::Mul(left, right) => left.evaluate(inputs) * right.evaluate(inputs), Gate::Div(left, right) => left.evaluate(inputs) / right.evaluate(inputs), Gate::Const(val) => *val, }; self.value = Some(result); result } pub fn to_solidity(&self, counter: &mut usize) -> String { match &self.gate { Gate::Input(name) => format!("{}[{}]", name, counter), Gate::Add(left, right) => { let left_sol = left.to_solidity(counter); *counter += 1; let right_sol = right.to_solidity(counter); *counter += 1; format!("{} + {}", left_sol, right_sol) } Gate::Sub(left, right) => { let left_sol = left.to_solidity(counter); *counter += 1; let right_sol = right.to_solidity(counter); *counter += 1; format!("{} - {}", left_sol, right_sol) } Gate::Mul(left, right) => { let left_sol = left.to_solidity(counter); *counter += 1; let right_sol = right.to_solidity(counter); *counter += 1; format!("{} * {}", left_sol, right_sol) } Gate::Div(left, right) => { let left_sol = left.to_solidity(counter); *counter += 1; let right_sol = right.to_solidity(counter); *counter += 1; format!("{} / {}", left_sol, right_sol) } Gate::Const(val) => format!("{}", val), } } } impl Clone for Node { fn clone(&self) -> Self { Node { gate: match &self.gate { Gate::Input(name) => Gate::Input(name.clone()), Gate::Add(left, right) => Gate::Add(left.clone(), right.clone()), Gate::Sub(left, right) => Gate::Sub(left.clone(), right.clone()), Gate::Mul(left, right) => Gate::Mul(left.clone(), right.clone()), Gate::Div(left, right) => Gate::Div(left.clone(), right.clone()), Gate::Const(val) => Gate::Const(*val), }, value: self.value, } } } pub struct ArithmeticCircuit { root: Node, } impl ArithmeticCircuit { pub fn new(root: Node) -> Self { ArithmeticCircuit { root } } pub fn from_expression(expr: &str) -> Self { let root = Self::parse_expression(expr).expect("Failed to parse expression").1; ArithmeticCircuit::new(root) } fn parse_expression(input: &str) -> IResult<&str, Node> { let (input, init) = Self::parse_term(input)?; fold_many0( pair(alt((char('+'), char('-'))), Self::parse_term), move || init.clone(), |acc, (op, val): (char, Node)| { if op == '+' { Node::new(Gate::Add(Box::new(acc), Box::new(val))) } else { Node::new(Gate::Sub(Box::new(acc), Box::new(val))) } }, )(input) } fn parse_term(input: &str) -> IResult<&str, Node> { let (input, init) = Self::parse_factor(input)?; fold_many0( pair(alt((char('*'), char('/'))), Self::parse_factor), move || init.clone(), |acc, (op, val): (char, Node)| { if op == '*' { Node::new(Gate::Mul(Box::new(acc), Box::new(val))) } else { Node::new(Gate::Div(Box::new(acc), Box::new(val))) } }, )(input) } fn parse_factor(input: &str) -> IResult<&str, Node> { alt(( map_res(digit1, |digit_str: &str| { i32::from_str(digit_str).map(|val| Node::new(Gate::Const(val))) }), map(Self::parse_identifier, |id: &str| { Node::new(Gate::Input(id.to_string())) }), delimited( char('('), Self::parse_expression, char(')'), ), ))(input) } fn parse_identifier(input: &str) -> IResult<&str, &str> { nom::character::complete::alpha1(input) } pub fn evaluate(&mut self, inputs: &[(&str, i32)]) -> i32 { let input_map: HashMap<String, i32> = inputs.iter().cloned().map(|(k, v)| (k.to_string(), v)).collect(); self.root.evaluate(&input_map) } pub fn to_solidity(&self) -> String { let mut counter = 0; let body = self.root.to_solidity(&mut counter); format!( r#" pragma solidity ^0.8.0; contract ArithmeticCircuit {{ function verify(int256[] memory x, int256[] memory y) public pure returns (int256) {{ return {}; }} }} "#, body ) } }
Step 2: Update the Main Function
Update the main
function to generate the Solidity smart contract code based on the parsed expression.
src/main.rs
use arithmetic_circuit::circuit::ArithmeticCircuit; fn main() { let expr = "(x+y)*y"; let mut circuit = ArithmeticCircuit::from_expression(expr); let result = circuit.evaluate(&[("x", 2), ("y", 3)]); println!("Result: {}", result); // Should print 15 let solidity_code = circuit.to_solidity(); println!("{}", solidity_code); }
Step 3: Run the Project
Run the project to see the generated Solidity code.
cargo run
Output
The output should be the Solidity code for a contract that can verify the proof for the given arithmetic circuit. For the expression (x + y) * y
, the generated Solidity code would look like this:
pragma solidity ^0.8.0; contract ArithmeticCircuit { function verify(int256[] memory x, int256[] memory y) public pure returns (int256) { return x[0] + y[1] * y[2]; } }
You can find the full implementation in my Github repo here.
You now have a Rust program that can parse arithmetic expressions, build arithmetic circuits, evaluate them, and generate Solidity code for a smart contract to verify the proof. This example can be extended to handle more complex expressions and additional operations, making it a versatile tool for generating verifiable computations in Solidity.

๐ Discover More Free Software Engineering Content! ๐
If you enjoyed this post, be sure to explore my new software engineering blog, packed with 200+ in-depth articles, ๐ฅ explainer videos, ๐๏ธ a weekly software engineering podcast, ๐ books, ๐ป hands-on tutorials with GitHub code, including:
๐ Developing a Fully Functional API Gateway in RustโโโDiscover how to set up a robust and scalable gateway that stands as the frontline for your microservices.
๐ Implementing a Network Traffic AnalyzerโโโEver wondered about the data packets zooming through your network? Unravel their mysteries with this deep dive into network analysis.
๐Implementing a Blockchain in Rustโโโa step-by-step breakdown of implementing a basic blockchain in Rust, from the initial setup of the block structure, including unique identifiers and cryptographic hashes, to block creation, mining, and validation, laying the groundwork.
and much more!
โ
200+ In-depth software engineering articles
๐ฅ Explainer VideosโโโExplore Videos
๐๏ธ A brand-new weekly Podcast on all things software engineeringโโโListen to the Podcast
๐ Access to my booksโโโCheck out the Books
๐ป Hands-on Tutorials with GitHub code
๐ Book a Call
๐ Visit, explore, and subscribe for free to stay updated on all the latest: Home Page
LinkedIn Newsletter: Stay ahead in the fast-evolving tech landscape with regular updates and insights on Rust, Software Development, and emerging technologies by subscribing to my newsletter on LinkedIn. Subscribe Here
๐ Connect with Me:
- LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
- X: Follow me on Twitter for quick updates and thoughts on Rust programming. Follow on Twitter
Wanna talk? Leave a comment or drop me a message!
All the best,
Luis Soares
luis@luissoares.dev
Lead Software Engineer | Blockchain & ZKP Protocol Engineer | ๐ฆ Rust | Web3 | Solidity | Golang | Cryptography | Author