Implementing Zero Knowledge Multi-Party Computation in Rust
Luis Soares, M.Sc.
Lead Software Engineer | Blockchain & ZK Protocol Engineer | ?? Rust | C++ | Web3 | Solidity | Golang | Cryptography | Author
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
The technical implementation of MPC involves several key components and steps:
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.
cargo run --bin verifier
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.
cargo run --bin prover
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).
cargo run --bin user_tui
nc localhost 7878
{"from":"Alice","to":"Bob","amount":100}
{"from":"Charlie","to":"Dave","amount":50}
Observing the Interaction
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:
?? Connect with Me:
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
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 :-)
Thanks for sharing