Tutorial 1: Create Your First ZKWASM Application
Overview:
WebAssembly (WASM) is an architecture-independent bytecode that can be compiled from various widely used programming languages, including C, Rust, and JavaScript.
ZKWASM is a virtual machine for WebAssembly that executes WASM bytecode and generates zero-knowledge proofs for the execution results. These proofs enable the other party to verify the correctness of the program’s execution without having to re-run the entire program. This is referred to as a “succinct proof,” which maintains a constant size regardless of the program’s complexity.
Use cases for ZKWASM
Wondering when to consider using ZKWASM? Here are five scenarios where ZKWASM can offer significant advantages:
Zero-knowledge proofs without circuit complexity
If you want to generate a zero-knowledge proof (ZKP), but you find writing ZKP circuits tedious or overly complex, ZKWASM can help. Instead of writing custom ZKP circuits, you can write a program in a conventional language, compile it into WebAssembly, and use ZKWASM to handle the rest. This allows you to prove the execution of a program off-chain or offline without the need for complex ZKP circuit design.
Offloading expensive on-chain computation
If your program includes heavy computational tasks like complex signature verification or cryptographic hashing, running it directly on-chain can be expensive. In this case, you can write the program in WebAssembly and perform the computations off-chain. ZKWASM will generate a proof that you can submit to the blockchain, allowing the chain to verify the result without having to perform the expensive computations itself.
Simplifying development with traditional languages
When managing large codebases—sometimes with over 100,000 lines of code—the lack of good package management tools and development aids in some blockchain programming languages like solidity can be frustrating. ZKWASM allows you to develop your application using traditional languages like C or Rust, which can be more efficient in terms of project management. You can then compile these programs into WebAssembly, making the development process smoother and more manageable.
Unified logic for on-chain and off-chain applications
A key use case we promote is when you’re developing a browser application (such as a decentralized game) that requires both on-chain and off-chain logics. Typically, you’d have to write two versions of your code: one for the on-chain logic and the other for the off-chain logic. With ZKWASM, you can write your program in a language supported by WebAssembly and use the same logic for both on-chain and off-chain processes. We will be covering this in an upcoming lesson, where we’ll walk through building a browser-based application that integrates with ZKWASM, enabling both on-chain verification and off-chain execution.
Supporting large applications and custom frameworks
In the fifth use case, if your application is relatively large—such as when you’re building a foundational framework or a tool like a configuration system—you might want to provide your users with the ability to define custom logics. In these scenarios, ZKWASM can be a great option. Users can write simple programs in traditional languages, which can then be compiled into ZKP circuits. These custom circuits can be combined with the circuits you’ve already written, allowing users to integrate their logics without needing to understand how to create zero-knowledge proof circuits by themselves.
Code Examples
Example 1: Non-input application
(More rust examples can be find at https://github.com/ZhenXunGe/zkWasm-rust-template)
Although ZKWASM supports WebAssembly, it’s important to note that some of the APIs we normally use, especially system-level calls, may not work directly. For example, WebAssembly in a ZKWASM environment might not support typical APIs like stdout. So, how can we bypass these limitations while still achieving the desired outcome? That’s what I’ll demonstrate in our first example, we’ll write a small C program that returns the value 2333333.
__attribute__((visibility("default")))
int zkmain() {
return 233333;
}
To compile this simple C program into WebAssembly we can use clang as follows:
FLAGS = -flto -O3 -nostdlib -fno-builtin -ffreestanding -mexec-model=reactor --target=wasm32 -Wl,--strip-all -Wl,--initial-memory=131072 -Wl,--max-memory=131072 -Wl,--no-entry -Wl,--allow-undefined -Wl,--export-dynamic
clang -o output.wasm zkmain.c $(FLAGS)
Once compiled, we’ll get a program called output.wasm. We can upload the image at https://explorer.zkwasmhub.com by clicking on “Create New Application.” (For this demo, every operation will be carried out in the cloud for simplicity, but you can also run it locally via command lines. )
The pending Setup task is the image just uploaded.
When the program is executed, no parameter is needed.
After signature, we can see a new Prove task.
After the Prove task is done, you will get some proof details such as Batch Instances, Proof Transcripts and Aux Data.
After the proof is generated, you can deploy a contract to Ethereum, allowing you to verify the computation on-chain. This entire process only requires four main API calls: Create New Application, Submit Prove Task, Deploy Verification Contract (usually done by zkwasmhub itself), and Verify in task details.
When clicking on the “verify” button, you can test the verification contract by submitting the proof to it.
Example 2: Handling inputs in ZKWASM programs
In this second example, I’ll modify the program to accept inputs from stdin, allowing it to perform more meaningful operations. Although APIs like printf and stdout aren’t supported in ZKWASM, we can work around these limitations by focusing on supported instructions. WebAssembly currently has around 147 core instructions, but many host-dependent features, such as I/O operations, are not natively supported in the virtual machine.
In the following picture, white bocks are not supported yet, and most of them are float point number instructions.
In this second example, I want to read an external input. Specifically, I want the input to be the length of a string—let’s say the string “dengLian”, it has a length of 8 characters. So, the program should verify if the input length is indeed 8.
In the program, I retrieve an input from stdin, which is defined as an integer (int), although the type could be changed to unsigned int or int64 if necessary. After compiling the program, we end up with a slightly more complex binary.
void require(int cond);
unsigned long long wasm_input(int);
char* string = "denglian\0";
void zkmain() {
int sum = wasm_input(1); // read public input
for (int i=0; string[i]!="\0"; i++) {
sum -= 1;
}
require(sum == 0);
}
Think of that you’ve set up a contract for each image, though not in the typical sense of solidity contracts. ZKWASM generates hash related initial parameters that can identify the images. Once we’ve set up the new program, we can run it, just like how we did in the earlier example.
You don’t have to use the ZKWASM explorer for this process. We provide a JavaScript library that you can use to interact with ZKWASM’s APIs. You can use this library to handle tasks like submitting proofs and deploying contracts. The library, available through the “ServiceHelper” API (https://github.com/DelphinusLab/zkWasm-service-helper), making it easier to automate the submission of tasks and generation of proofs. You can also write your own wrapper around the API if you prefer.
For local development, you can download the ZKWASM repository (https://github.com/DelphinusLab/zkWasm) and run everything on your machine. The core APIs are the same regardless of whether you’re running it locally or through our cloud service. There are only four main API calls: setup, proof, verify, and deploy.
Now, let’s go back to our program that accepts an input—the length of the string “denglian”. If you input the correct length (which is 8), ZKWASM will successfully generate a proof.
However, if the input is incorrect, following the principles of zero-knowledge proofs, ZKWASM will not be able to generate a valid proof. Once the proof is generated, it can be submitted to the blockchain for verification.
The proof mainly includes
- Batch Instances
- Proof Transcripts, a fixed-length transcripts, typically u256 32 or 64
- Auxiliary data that reduce the amount of on-chain computation required to verify the proof.
If you have multiple proofs that you want to submit at once, ZKWASM can batch these proofs together. This allows you to submit multiple proofs without increasing the size of the final data being sent to the blockchain. The overall size of the proofs remains the same, regardless of how many computations you are proving.
The importance of Setup
One final key point about ZKWASM: when you are setting up a program, ZKWASM generates a kzg commitment to the image that will be executed. This ensures that when the program is running, it’s verified as the exact program that was committed during the setup phase. This commitment prevents anyone from tampering with the program or running a different program in its place.
Rust program into a browser environment
Let’s build a more complex example by embedding a WebAssembly (WASM) program into a browser environment.
In this example, we’ll use Rust to write a program, compile it to WebAssembly, and then integrate it into the browser. To start, we’ll use a simple Rust project.
We’ll use this repo: https://github.com/ZhenXunGe/zkWasm-rust-template
There’s a tool called wasm-bindgen, which allows you to compile Rust code into WebAssembly bytecode. When you’re writing src/lib.rs, you can turn zkmain into an entry function by adding#[wasm_bindgen] before zkmain function.
use wasm_bindgen::prelude::*;
use zkwasm_rust_sdk::{
wasm_input,
require,
};
#[wasm_bindgen]
pub fn zkmain() {
let data = vec![0x83, b'c', b'a', b't'];
let _animal: String = rlp::decode(&data).unwrap();
// assert_eq!(animal, "cat".to_owned());
//
}
In our previous demos, you may have noticed that every program starts from a function named zkmain. Similarly, in your Rust code, you must specify the entry point that exposes your program to the WebAssembly runtime.
Once your Rust program is ready, you can compile it using wasm-pack build –release, which generates WebAssembly program, and it’s called “zkwasm_rlp_bg.wasm” in our example. And you can check this program with command wasm2wat. This bytecode can then be executed inside the ZKWASM virtual machine.
You can run this program directly on your local machine and the process remains the same: we use four basic API calls to set up the environment, generate a proof, verify the proof, and interact with the blockchain.
You can batch proofs or one proof with our batching tool from https://github.com/DelphinusLab/continuation-batcher
- Add new host circuits: https://github.com/DelphinusLab/zkWasm-host-circuits
- Customize your proof batching scripts: https://github.com/DelphinusLab/halo2aggregator-s
- Continuation support: https://github.com/DelphinusLab/continuation-batcher
A stateful program
Moving on to the next example: creating a stateful program. So far, we’ve worked with stateless programs that don’t access a database or modify any state. However, to build more complex applications, such as a game or a decentralized app (dApp), you need to handle state, signatures, and other dynamic elements.
For this example, we’ll create a more advanced program that can sign data, verify signatures, and interact with ZKWASM. Specifically, the program will use cryptographic signatures and produce proofs that can be verified on-chain. Each time a user submits a proof to the blockchain, it must reference the previous proof, allowing for incremental validation.
And the following picture demonstrates the code:
We have a zkmain, load hasher, and load the merkle tree and then change it. After this, add some merkle tree leaves and then verify signatures. (Full code can be accessed at https://github.com/ZhenXunGe/zkWasm-rust-template/tree/main/settlement)
Once the program is compiled, you can run it locally or in the cloud and generate the proof.
By the end of this example, you’ll have a stateful application capable of generating zk-SNARK proofs, validating signatures, and interacting with the blockchain for settlement.
One Comment
Comments are closed.