Implementing Zero Knowledge Multi-Party Computation in Rust

Implementing Zero Knowledge Multi-Party Computation in Rust

Hi there, fellow Rustaceans! ??

In this tutorial, we’ll implement a simplified zk-rollup-like system using Rust and the concept of Multi-Party Computation.

This system involves three main components: a User interface for submitting transactions, a Prover (or Operator) that batches transactions and generates proofs, and a Verifier (akin to a mainnet) that checks these proofs.

The focus will be ensuring secure and efficient communication between these components, handling challenges and responses, and building a simple Terminal User Interface (TUI) for user interactions.

Let′s dive right in! ??

Multi-party computation (MPC)

Multi-Party Computation (MPC) is a subfield of cryptography that enables multiple parties to jointly compute a function over their inputs while keeping those inputs private. The fundamental goal of MPC is to ensure that, during the computation process, no party learns anything more about the other parties’ inputs than what can be inferred from the output.

Core Principles

  • Privacy: Each party’s input data remains confidential, even from other participants in the computation.
  • Correctness: The result of the computation is correct and verifiable, as if it were computed by a trusted third party.
  • Independence: No single party can control the outcome of the computation beyond the influence of their own input.

The technical implementation of MPC involves several key components and steps:

  • Secure Function Evaluation (SFE): The function to be computed is represented in a manner that allows for secure evaluation, often using cryptographic primitives. This can include representing the function as a circuit of logical gates.
  • Secret Sharing: Inputs are split into “shares” that are distributed among the parties. Each share is meaningless on its own, but collectively, the shares can reconstruct the original input.
  • Computation on Shares: The parties engage in a protocol to compute the function over their shares. This often involves exchanging messages that contain encrypted or obfuscated data derived from the shares.
  • Result Reconstruction: After the computation, the parties combine their resulting shares to obtain the final output. The protocol ensures that this step reveals no additional information about the individual inputs.

In this tutorial, we explore the foundational concepts and practical implementation of a simplified zk-rollup-like system, drawing conceptual parallels to Multi-Party Computation (MPC). Although our focus isn’t explicitly on MPC, the underlying principles of privacy, correctness, and collaborative computation are central to both domains.

Implementing the the Prover (Operator)

The Prover begins by listening for incoming connections from users intending to submit transactions. This is achieved by creating a TcpListener bound to a specific port:

let listener = TcpListener::bind("localhost:7878").expect("Could not bind to port 7878");

println!("Operator listening on port 7878");        

This code snippet sets up the Prover to listen on port 7878, ready to accept transaction data from users.

Collecting Transactions

As users connect and submit their transactions, the Prover reads this data using a BufReader, which efficiently manages the stream's data:

for stream in listener.incoming() {
    let stream = stream.expect("Failed to accept incoming connection");

    println!("User connected");

    let mut reader = BufReader::new(stream);
   
    let mut transaction_data = String::new();
    
    reader.read_line(&mut transaction_data).expect("Failed to read from user");
    // Process transaction data...
}        

This loop waits for user connections, reading the transaction data sent by each user. The read_line method is used here for simplicity, assuming each transaction or batch of transactions ends with a newline character.

Generating the Proof

For each transaction, the Prover generates a hash, simulating a part of the proof generation process. These hashes are then combined to create a single “proof” hash:

let transaction_hashes: Vec<String> = transactions.iter().map(|tx| {
    let tx_json = serde_json::to_string(tx).unwrap();
    let mut hasher = Sha256::new();
    hasher.update(tx_json.as_bytes());
    encode(hasher.finalize())
}).collect();

let proof = generate_merkle_root(&transaction_hashes);        

This code iterates over each transaction, serializes it to JSON, computes its SHA-256 hash, and collects these hashes. The generate_merkle_root function (not shown here) would typically combine these hashes to produce a single hash representing the proof.

Communicating with the Verifier

After generating the proof, the Prover connects to the Verifier and sends the transaction hashes and the proof:

let mut verifier_stream = TcpStream::connect("localhost:7879").expect("Could not connect to verifier");

let proof_data = json!({
    "transaction_summary": transaction_hashes,
    "proof": proof
});

let serialized = serde_json::to_string(&proof_data).unwrap();

verifier_stream.write_all(serialized.as_bytes()).expect("Failed to write to verifier stream");        

This snippet establishes a connection to the Verifier (assumed to be listening on port 7879), packages the transaction hashes and the proof into a JSON object, serializes it, and sends it over the TCP stream.

Handling the Verifier’s Challenge

Finally, the Prover listens for a challenge from the Verifier, responds with the requested data, and closes the connection:

let mut challenge = String::new();

reader.read_line(&mut challenge).expect("Failed to read challenge from verifier");

let challenge_index: usize = challenge.trim().parse().expect("Failed to parse challenge");

let response = transaction_hashes[challenge_index].clone();

verifier_stream.write_all(response.as_bytes()).expect("Failed to respond to challenge");        

In this phase, the Prover reads the challenge issued by the Verifier (which, for simplicity, we can assume is an index requesting a specific transaction hash), retrieves the corresponding hash from transaction_hashes, and sends this hash back as the response.

Implementing de Verifier

In this section, we explore the Verifier’s role in our simplified zk-rollup-like system, complemented by relevant code snippets to illustrate the implementation details.

Initiating the Verifier

The Verifier starts by setting up a TcpListener to listen for incoming connections from the Prover, indicating the arrival of a new batch of transactions and the corresponding proof:

let listener = TcpListener::bind("localhost:7879").expect("Could not bind to port 7879");

println!("Mainnet listening on port 7879");        

This code snippet establishes the Verifier to listen on port 7879, ready to receive data from the Prover.

Receiving Proof and Transaction Hashes

Upon connection, the Verifier reads the transmitted proof and transaction summaries using a BufReader for efficient data handling:

for stream in listener.incoming() {

    let stream = stream.expect("Failed to accept incoming connection");
    
    println!("Operator connected");

    let mut reader = BufReader::new(stream);
    
    let mut proof_data_string = String::new();
    
    reader.read_line(&mut proof_data_string).expect("Failed to read from stream");
    
    let proof_data: Value = serde_json::from_str(&proof_data_string).expect("Failed to parse proof data");
    // Further processing...
}        

This loop listens for connections from the Prover and reads the transmitted JSON data, which includes the transaction hashes and the generated proof.

Issuing a Challenge

The Verifier then issues a challenge to the Prover. This typically involves requesting specific information to validate the proof, such as a particular transaction hash:

let challenge = thread_rng().gen_range(0..transaction_summary.len());

write!(stream, "{}\n", challenge).expect("Failed to send challenge to operator");        

This snippet selects a random transaction hash index as the challenge and sends this index back to the Prover, expecting the Prover to respond with the corresponding transaction hash.

Verifying the Response

Upon receiving the Prover’s response, the Verifier compares the provided transaction hash against its record to validate the proof:

let mut response = String::new();

reader.read_line(&mut response).expect("Failed to read response from operator");

let response = response.trim_end();

let verified = transaction_summary.get(challenge).map_or(false, |expected_hash| {
    expected_hash.trim_matches('"') == response
});        

Here, the Verifier reads the Prover’s response and trims any potential newline characters. The verification involves comparing the received hash (response) with the expected hash from the transaction summary. If they match, the proof is considered valid.

Logging and Finalization

Finally, the Verifier logs the result of the verification process and takes appropriate actions based on the outcome:

if verified {
    println!("Verification Success: Transactions committed to the blockchain.");
} else {
    println!("Verification Failed: Invalid proof. Transactions not committed.");
}        

This concluding code logs whether the verification succeeded or failed. A successful verification implies that the transactions are valid and can be “committed” to the blockchain. In contrast, a failure indicates an issue with the proof or the batched transactions, preventing their commitment.

Putting it all together

Here is the full prover.rs implementation:

use std::net::{TcpListener, TcpStream};
use std::io::{BufRead, BufReader, Write};
use serde::{Serialize, Deserialize};
use serde_json;
use sha2::{Digest, Sha256};
use hex::encode;

#[derive(Serialize, Deserialize, Debug)]
struct Transaction {
    from: String,
    to: String,
    amount: u64,
}

fn main() {
    let listener = TcpListener::bind("localhost:7878").expect("Could not bind to port 7878");
    println!("Operator listening on port 7878");

    loop {
        // Accepting transactions from users
        let (user_stream, _) = listener.accept().expect("Failed to accept user connection");
        println!("User connected");

        let mut user_reader = BufReader::new(user_stream);
        let mut transactions = Vec::new();
        let mut line = String::new();

        // Read transactions from the user
        while user_reader.read_line(&mut line).expect("Failed to read from user") > 0 {
            if let Ok(tx) = serde_json::from_str::<Transaction>(&line) {
                transactions.push(tx);
            }
            line.clear();
        }

        if !transactions.is_empty() {
            // Process transactions and generate proof
            let transaction_hashes = generate_transaction_hashes(&transactions);
            let proof = generate_merkle_root(&transaction_hashes);

            // Connect to the verifier and send proof along with transaction hashes
            let mut verifier_stream = TcpStream::connect("localhost:7879").expect("Could not connect to verifier");
            let proof_data = serde_json::json!({
                "transaction_summary": transaction_hashes,
                "proof": proof
            });

            let serialized = serde_json::to_string(&proof_data).unwrap();
            verifier_stream.write_all(serialized.as_bytes()).expect("Failed to write to verifier stream");
            verifier_stream.write_all(b"\n").expect("Failed to write newline to verifier stream");
            println!("Proof and transaction hashes sent to verifier.");

            // Listen for a challenge from the verifier and respond
            let mut verifier_reader = BufReader::new(verifier_stream);
            let mut challenge = String::new();
            verifier_reader.read_line(&mut challenge).expect("Failed to read challenge from verifier");
            let challenge: usize = challenge.trim().parse().expect("Failed to parse challenge");

            if challenge < transaction_hashes.len() {
                println!("Sending response hash: {}", transaction_hashes[challenge]);
                verifier_reader.get_mut().write_all(transaction_hashes[challenge].as_bytes()).expect("Failed to respond to challenge");
                verifier_reader.get_mut().write_all(b"\n").expect("Failed to write newline after challenge response");
            }
        }
    }
}

fn generate_transaction_hashes(transactions: &[Transaction]) -> Vec<String> {
    transactions.iter().map(|tx| {
        let tx_json = serde_json::to_string(tx).unwrap();
        let mut hasher = Sha256::new();
        hasher.update(tx_json.as_bytes());
        encode(hasher.finalize())
    }).collect()
}

fn generate_merkle_root(transaction_hashes: &[String]) -> String {
    let concatenated_hashes = transaction_hashes.concat();
    let mut hasher = Sha256::new();
    hasher.update(concatenated_hashes.as_bytes());
    encode(hasher.finalize())
}        

Finally, the user_tui.rs implementation:

use cursive::views::{Dialog, EditView, LinearLayout, TextView};
use cursive::{Cursive, CursiveExt};
use std::net::TcpStream;
use std::io::Write;
use cursive::traits::Resizable;
use serde_json::json;
use cursive::view::Nameable;
fn main() {
    let mut siv = Cursive::default();

    siv.add_layer(
        Dialog::new()
            .title("Send Transaction")
            .content(
                LinearLayout::vertical()
                    .child(TextView::new("From:"))
                    .child(EditView::new().with_name("from").fixed_width(20))
                    .child(TextView::new("To:"))
                    .child(EditView::new().with_name("to").fixed_width(20))
                    .child(TextView::new("Amount:"))
                    .child(EditView::new().with_name("amount").fixed_width(20)),
            )
            .button("Send", |s| {
                let from = s.call_on_name("from", |v: &mut EditView| v.get_content()).unwrap();
                let to = s.call_on_name("to", |v: &mut EditView| v.get_content()).unwrap();
                let amount = s.call_on_name("amount", |v: &mut EditView| v.get_content()).unwrap();

                if let Ok(amount) = amount.parse::<u64>() {
                    send_transaction(&from, &to, amount);
                    s.add_layer(Dialog::info("Transaction sent!"));
                } else {
                    s.add_layer(Dialog::info("Invalid amount!"));
                }
            })
            .button("Quit", |s| s.quit()),
    );

    siv.run();
}

fn send_transaction(from: &str, to: &str, amount: u64) {
    let transaction = json!({
        "from": from,
        "to": to,
        "amount": amount
    });

    if let Ok(mut stream) = TcpStream::connect("localhost:7878") {
        let serialized = serde_json::to_string(&transaction).unwrap() + "\n";
        if let Err(e) = stream.write_all(serialized.as_bytes()) {
            eprintln!("Failed to send transaction: {}", e);
        }
    } else {
        eprintln!("Could not connect to prover");
    }
}        

The updated Cargo.toml:

[package]
name = "rust-mpc"
version = "0.1.0"
edition = "2021"
authors = ["Luis Soares"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10.2"
hex = "0.4.3"
rand = "0.8.5"
cursive = "0.20.0"

[[bin]]
name = "prover"
path = "src/prover.rs"

[[bin]]
name = "verifier"
path = "src/verifier.rs"

[[bin]]
name = "user_tui"
path = "src/user_tui.rs"        

Now, let′s test it!

1. Start the Verifier

Begin by launching the Verifier, which listens for proofs and transaction summaries from the Prover and issues challenges for verification.

  • Open a terminal window.
  • Navigate to your project directory.
  • Start the Verifier using Cargo:

cargo run --bin verifier        

  • Keep this terminal open to monitor the Verifier’s activity and outputs.

2. Launch the Prover

With the Verifier running, start the Prover, which aggregates transactions from Users, generates proofs, and interacts with the Verifier for validation.

  • Open a new terminal window.
  • Navigate to your project directory.
  • Run the Prover:

cargo run --bin prover        

  • Monitor this terminal to observe the Prover’s operations, including receiving transactions from Users and communicating with the Verifier.

3. Use the User Interface to Send Transactions

If you’ve implemented a User interface (such as the TUI discussed earlier), launch it to start submitting transactions. If a TUI is not available, you can simulate User transactions by manually sending JSON-formatted transaction data to the Prover using tools like nc (netcat).

  • Open another terminal window for the User interface.
  • If using a TUI, start it with:

cargo run --bin user_tui        

  • If manually sending transactions, connect to the Prover using nc and enter transaction data:

nc localhost 7878 
{"from":"Alice","to":"Bob","amount":100} 
{"from":"Charlie","to":"Dave","amount":50}        

  • Press Enter after each transaction and use Ctrl+D (Linux/Mac) or Ctrl+Z followed by Enter (Windows) to end the transmission.

Observing the Interaction

  • On the Verifier’s Terminal: Watch for connections from the Prover, incoming transaction hashes and proofs, the challenge issued, and the result of the verification process.
  • On the Prover’s Terminal: Look for connections from Users, incoming transactions, the proof generation process, communication with the Verifier, the challenge received, and the response sent.
  • On the User Interface Terminal: If using the TUI, follow the prompts to input and send transactions. If manually sending transactions, ensure the JSON data is correctly formatted.

You can find the project on my Github here .

?? Explore More by Luis Soares

?? Learning Hub: Expand your knowledge in various tech domains, including Rust, Software Development, Cloud Computing, Cyber Security, Blockchain, and Linux, through my extensive resource collection:

  • Hands-On Tutorials with GitHub Repos: Gain practical skills across different technologies with step-by-step tutorials, complemented by dedicated GitHub repositories. Access Tutorials
  • In-Depth Guides & Articles: Deep dive into core concepts of Rust, Software Development, Cloud Computing, and more, with detailed guides and articles filled with practical examples. Read More
  • E-Books Collection: Enhance your understanding of various tech fields with a series of e-Books, including titles like “Mastering Rust Ownership” and “Application Security Guide” Download eBook
  • Project Showcases: Discover a range of fully functional projects across different domains, such as an API Gateway, Blockchain Network, Cyber Security Tools, Cloud Services, and more. View Projects
  • 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:

  • Medium: Read my articles on Medium and give claps if you find them helpful. It motivates me to keep writing and sharing Rust content. Follow on Medium
  • LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
  • Twitter: 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 [email protected]

Senior Software Engineer | Cloud Engineer | SRE | Tech Lead | Rust | Golang | Java | ML AI & Statistics | Web3 & Blockchain

Sukanta Ganguly, PhD, MBA

I build great products and grow companies

7 个月

Luis Soares, M.Sc. Your posts are really insightful. Love to talk to you, Will email you :-)

要查看或添加评论,请登录

Luis Soares, M.Sc.的更多文章

社区洞察

其他会员也浏览了