ZK Rollup Tutorial Part 3
Once you have a sequencer node up and running, it’s time to build a prover for your Sovereign Rollup. The prover generates ZK proofs for the correctness of transactions in blocks produced by the sequencer. It is the prover that makes this a Sovereign ZK Rollup, allowing light nodes to verify that its state transitions follow the rules of the EVM.
This page will lead you through the steps required to implement a working prover that creates proofs based on blocks received from the sequencer.
The most fundamental component of the prover is the functionality to generate ZK proofs for blocks. The details of this process are already implemented in Zeth (with includes Reth), but we still need to write a wrapper to allow us to interact with the Zeth block prover.
First, let’s use the clap crate to define a command line interface for the proof generation tool in the prover/src/main.rs
file. Clap can be used to define the command line arguments for the prover command, called evm-prover
as per the prover/Cargo.toml
file.
prover/src/main.rs
#[derive(Parser, Debug)]
#[clap(author, version, about = "Ethereum Proof Generation Tool")]
struct Args {
// Defines the sequencer's RPC endpoint
#[clap(long, default_value = "<http://localhost:8545>")]
rpc: String,
// The start block height to derive a proof for
#[clap(long, default_value = "5")]
start_block: u64,
// The amount of blocks to prove
#[clap(long, default_value = "10")]
batch_size: u64,
// Polling interval in seconds
#[clap(long, default_value = "5")]
interval: u64,
// Directory for Zeth binaries
#[clap(long)]
zeth_binary_dir: Option<String>,
// Level of urgency for logs
#[clap(long, default_value = "info")]
log_level: String,
}
Once that’s done, we can start on the block prover function in the prover/src/main.rs
file. As mentioned, it functions as a wrapper for the zeth-ethereum prove
command, which is obtained from the Zeth binaries we compiled in Getting Started. This function will take the command line arguments we defined above and use them to run Zeth’s prove
command.
prover/src/main.rs
// Prover function
// Takes in the CLI arguments defined above
fn run_ethereum_prove(
rpc: &str,
block_number: u64,
batch_size: u64,
zeth_binary_dir: Option<String>,
log_level: &str,
) -> Result<(), String> {
// Log message
info!(
"Running Ethereum prove for blocks {}-{}",
block_number,
block_number + batch_size - 1
);
// Attempt to create path to Zeth binaries
let mut binary_path = if let Some(dir) = &zeth_binary_dir {
PathBuf::from(dir)
} else {
debug!("No binary directory provided, trying current directory");
std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?
};
binary_path.push("zeth-ethereum");
// Ensure binary path exists
if !binary_path.exists() {
return Err(format!("Binary not found at: {}", binary_path.display()));
}
// Run the zeth-ethereum prove command with the provided arguments
let output = Command::new(&binary_path)
.env("RUST_LOG", log_level)
.args([
"prove",
&format!("--rpc={}", rpc),
&format!("--block-number={}", block_number),
&format!("--block-count={}", batch_size),
])
.output()
.map_err(|e| format!("Failed to execute zeth-ethereum prove: {}", e))?;
// Error messages
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("ethereum prove command failed: {}", stderr);
return Err(format!(
"ethereum prove command failed with status: {}\\nStderr: {}",
output.status, stderr
));
}
info!("Successfully processed batch");
Ok(())
}
An important responsibility of the prover is periodically querying the latest block height and using it to generate ZK proofs for new blocks. In the prover/src/main.rs
file, we can define a function that will send a request to the sequencer node to get the latest block height.
prover/src/main.rs
// Get the latest block height
// Takes in a reqwest blocking client and RPC endpoint for the sequencer
fn get_latest_block(client: &Client, rpc: &str) -> Result<u64, String> {
debug!("Checking latest block height...");
// Create a JSON request
let request_body = json!({
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
});
// Send the request to the sequencer and record the response
let response = client
.post(rpc)
.json(&request_body)
.send()
.map_err(|e| format!("Failed to send request: {}", e))?;
if !response.status().is_success() {
return Err(format!("Request failed with status: {}", response.status()));
}
let response_json: Value = response
.json()
.map_err(|e| format!("Failed to parse response JSON: {}", e))?;
// Extract the latest block height
let block_hex = response_json
.get("result")
.and_then(Value::as_str)
.ok_or("Failed to parse response")?
.trim_start_matches("0x");
let block_number = u64::from_str_radix(block_hex, 16)
.map_err(|e| format!("Failed to parse hex block number: {}", e))?;
debug!("Latest block: {}", block_number);
Ok(block_number)
}
The Zeth prover that is run by the run_ethereum_prove()
function above stores its proofs in .zkp
files. To allow third parties to access these proofs, we need to run a server that can return proofs for block ranges specified in requests. First, let’s define a handler for these requests in the prover/src/http.rs
file.
prover/src/http.rs
// Define a struct for a proof request
// Includes the start block and number of blocks in the proof
// Uses serde crate to enable struct to be read from JSON file via the Deserialize trait
#[derive(Deserialize)]
pub struct ProofRequest {
block_start: u64,
block_count: u64,
}
/// Handler for GET request to the prover server
pub async fn serve_proof(Query(query): Query<ProofRequest>) -> Response {
// Open proof file containing requested block range
let file_name = format!(
"{}-{}.zkp",
query.block_start,
query.block_count + query.block_start
);
let path = PathBuf::from(&file_name);
// Read file contents
match fs::read(&path).await {
Ok(bytes) => (StatusCode::OK, bytes).into_response(),
Err(err) => {
let status = if err.kind() == std::io::ErrorKind::NotFound {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
(status, format!("Error reading file: {}", err)).into_response()
}
}
}