Dark Mode Light Mode
Dark Mode Light Mode

Implementing an Arithmetic Circuit Compiler in Rust

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.

Click here to Learn More

๐Ÿš€ 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

Add a comment Add a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Understanding Zero-Knowledge Proofs: Encoding Programs into zk-SNARKs

Next Post

Introduction to Provable Smart Contracts