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.

1. Proving Blocks via Zeth’s Ethereum Prover

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, 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 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 process
    #[clap(long, default_value = "10")]
    batch_size: u64,

		// Delay 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(())
}

2. Getting the Latest Block

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 JSON-RPC

prover/src/main.rs

fn get_latest_block(client: &Client, rpc: &str) -> Result<u64, String> {
    debug!("Checking latest block height...");

    let request_body = json!({
        "jsonrpc": "2.0",
        "method": "eth_blockNumber",
        "params": [],
        "id": 1
    });

    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))?;

    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)
}

3. Main Function

prover/src/main.rs

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();

    std::thread::spawn(move || {
        if let Err(e) = run_server() {
            error!("Error running server: {}", e);
        }
    });

    let filter =
        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level));

    fmt::fmt().with_env_filter(filter).with_target(false).init();

    info!("Starting Ethereum prover...");

    let client = Client::new();
    let mut current_block = args.start_block;

    loop {
        match get_latest_block(&client, &args.rpc) {
            Ok(latest_block) => {
                if latest_block >= current_block + args.batch_size {
                    info!(
                        "New blocks available. Current: {}, Latest: {}",
                        current_block, latest_block
                    );

                    match run_ethereum_prove(
                        &args.rpc,
                        current_block,
                        args.batch_size,
                        args.zeth_binary_dir.clone(),
                        &args.log_level,
                    ) {
                        Ok(_) => {
                            current_block += args.batch_size;
                            info!("Updated current block to {}", current_block);
                        }
                        Err(e) => {
                            error!("Error running prover: {}", e);
                        }
                    }
                } else {
                    info!(
                        "No new blocks to process. Current: {}, Latest: {}, sleeping...",
                        current_block, latest_block
                    );
                    thread::sleep(Duration::from_secs(args.interval));
                }
            }
            Err(e) => {
                error!("Error getting latest block: {}", e);
                thread::sleep(Duration::from_secs(args.interval));
            }
        }
    }
}

4. Running the Prover Server

prover/src/http.rs