QuickStart
ZKC-Tutorial
In this guide, we will walk you through a casual Web3 game developed with ZKCross's SDK and service, it will cover the tech stack, requirements, development and deployment.
A simple demo of ZKC-SDK.
Technology Stack
- Front end
- Language: TypeScript v5
- Component engine: React v18
- Application framework: Next.js v13
- CSS utility class: Bootstrap v5
- Package management tool: pnpm
- CI / CD: GitHub Actions + Vercel
- WASM
How To Run This Repository
git clone --recurse-submodules https://github.com/zkcrossteam/ZKC-Tutorial.git
cd ZKC-Tutorial
Prerequisites
Node.js and pnpm are used in our tutorial.
pnpm i
pnpm dev
In this tutorial, the game logic is written with C and compiled into WASM, you need a modern C toolchain. We recommend using clang and llvm.
sudo apt install clang-15 lld-15 make
Getting started
In this section, we will provide a brief description of how to build this project. It is assumed that the reader is familiar with the C programming language and the front-end development environment. Basic concepts will not be covered in detail. For additional information about setting up the project's development environment, please refer to the document "How to run this repository".
Project Design and Implementation
This project will implement a dice game, in which the sum of the dice will be used as public input, and some values will be passed as witness (private inputs).
Create an nextjs app
npx create-next-app@latest ZKC-Tutorial
cd ZKC-Tutorial
git submodule add https://github.com/DelphinusLab/zkWasm-C zkWasm-C
git submodule update --init
Writing C Language Files and Makefile
Create a WASM folder and create C language files (such as example.c
) and a Makefile in the folder.
In a C file, You need to include <zkwasmsdk.h>
header file:
#include <zkwasmsdk.h>
You can create functions that can be called by external JavaScript by decorating __attribute__((visibility("default")))
before the function definition. Here is an example:
__attribute__((visibility("default")))
void setBoard(int input) {
if(times > 3) return;
board[times] = input;
times++;
result += input;
}
zkmain
is a required function that will be called for data validation each time zk proof is submitted. This function needs to be modified by __attribute__((visibility("default")))
. Here is the zkmain
funtion in this project:
__attribute__((visibility("default")))
int zkmain() {
// read the first public input, like 0x[0-f]*:i64
// e.g. 0x12:i64
int pubInput = (int)wasm_input(1);
// read the first private input, like 0x[0-f]*:i64
// e.g. 0x3:i64
int len = (int)wasm_input(0);
// read the second private input, like 0x[0-f]*:bytes-packed
// e.g. 0x060606:bytes-packed
read_bytes_from_u64(dices, len, 0);
init();
for(int i = 0; i < 3; i++){
setBoard(dices[i]);
}
// Validate input
require(pubInput == result);
return 0;
}
You'll notice that wasm_input
, read_bytes_from_u64
, and require
are called in zkmain
.
wasm_input
is a function used to get public input and witness, one value at one time. Public input can be read when its argument is 1
, and witness can be read when its argument is 0
.
The read_bytes_from_u64
function, which takes three arguments, can read public input and witness of type bytes-packed
. The first argument is a pointer to an array to record the read data, the second argument is the length of the read, and the third argument is 1
to read the public input and 0
to read witness. Each element in the array can store only one two-bytes data.
For example, public input(witness) is 0x01020304:bytes-packed
and the array is {0x01, 0x02, 0x03, 0x04}
. If the length of the array is longer than the data to be read, the remaining elements are filled with 0
; otherwise, there is no read overflow.
The require
function can take in a logical operation statement, which can only pass the verification if the logical operation statement is true
, otherwise the verification will fail.
The Makefile file can be populated with the following:
LIBS = -lkernel32 -luser32 -lgdi32 -lopengl32
SDKDIR = ../../zkWasm-C
CFLAGS = -Wall -I$(SDKDIR)/sdk/c/sdk/include/ -I$(SDKDIR)/sdk/c/hash/include/
# Should be equivalent to your list of C files, if you don't build selectively
CFILES = $(wildcard *.c)
ifeq ($(CLANG),)
CLANG=clang
endif
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
all: example.wasm
sdk.wasm:
sh $(SDKDIR)/sdk/scripts/build.sh sdk.wasm
example.wasm: $(CFILES) sdk.wasm
$(CLANG) -o $@ $(CFILES) sdk.wasm $(FLAGS) $(CFLAGS)
clean:
sh $(SDKDIR)/sdk/scripts/clean.sh
rm -f *.wasm *.wat
You also need to make the following changes to the above:
-
SDKDIR
should point to the SDK directory that you downloaded earlier. -
CLANG
should be consistent with local Clang command.The
CLANG
specified in all Makefile files in the SDK should be consistent with the local Clang command. You can check this by searching forCLANG
. -
You can replace
example.wasm
with the name of the file you want to generate.
Make
Open the directory where the command line tool created the makefile file in the previous step, and execute the following command:
make
Front End
TypeScript
If you are using TypeScript, you need to create three files:
- Create the file
types.d.ts
:
export interface MakeWasmOptions {
global: Record<string, any>;
env: {
memory: WebAssembly.Memory;
table: WebAssembly.Table;
abort: () => never;
require: (b: boolean | number) => void;
wasm_input: () => never;
};
}
export interface WasmModule {
instance: WebAssembly.Instance;
module: WebAssembly.Module;
}
- Create
packages.d.ts
and define the type for the.wasm
file:
declare module '*.wasm' {
const initWasm: (
makeWasmOptions: import('./types').MakeWasmOptions,
) => Promise<import('./types').WasmModule>;
export default initWasm;
}
- Create a TypeScript file to encapsulate functions that call functions in WASM:
// example.ts
import makeWasm from './example.wasm';
import { WasmModule } from './types';
const { Memory, Table } = WebAssembly;
let instance: WasmModule['instance'];
export interface WasmAPI {
setBoard: (input: number) => void;
getBoard: (index: number) => number;
getResult: () => number;
init: () => void;
}
async function initWasm<T = unknown>() {
if (instance != null) return instance.exports as T;
const wasmModule = await makeWasm({
global: {},
env: {
memory: new Memory({ initial: 10, maximum: 100 }),
table: new Table({ initial: 0, element: 'anyfunc' }),
abort: () => {
console.error('abort in wasm!');
throw new Error('Unsupported wasm api: abort');
},
require: b => {
if (!b) {
console.error('require failed');
throw new Error('Require failed');
}
},
wasm_input: () => {
console.error('wasm_input should not been called in non-zkwasm mode');
throw new Error('Unsupported wasm api: wasm_input');
},
},
});
console.log('module loaded', wasmModule);
/*
WebAssembly.instantiateStreaming(makeWasm, importObject).then(
(obj) => console.log(obj.instance.exports)
);
*/
instance = wasmModule.instance;
return instance.exports as T;
}
export default initWasm;
You need to make the following changes to the above file:
-
Confirm whether the import source file of
makeWasm
points to the previously created WASM; -
WasmAPI
is the interface exposed by WASM to javascript, and it needs to be modified to what you expect;
JavaScript
If you are using JavaScript, you only need to create a single file:
import makeWasm from './example.wasm';
const { Memory, Table } = WebAssembly;
let instance = null;
export default async function () {
if (instance != null) {
return instance.exports;
} else {
module = await makeWasm({
global: {},
env: {
memory: new Memory({ initial: 10, limit: 100 }),
table: new Table({ initial: 0, element: 'anyfunc' }),
abort: () => {
console.error('abort in wasm!');
throw new Error('Unsupported wasm api: abort');
},
require: b => {
if (!b) {
console.error('require failed');
throw new Error('Require failed');
}
},
wasm_input: () => {
console.error('wasm_input should not been called in non-zkwasm mode');
throw new Error('Unsupported wasm api: wasm_input');
},
},
});
console.log('module loaded', module); // "3
/*
WebAssembly.instantiateStreaming(makeWasm, importObject).then(
(obj) => console.log(obj.instance.exports)
);
*/
instance = module.instance;
return instance.exports;
}
}
You need to make the following changes to the above file:
- Confirm whether the import source file of
makeWasm
points to the previously created WASM;
Call Function in WASM at The Front End
Here is an example of calling initWasm
on the front-end page:
import initWasm, { WasmAPI } from '../wasm/example';
initWasm<WasmAPI>().then(({ init, setBoard, getResult }) => {
init();
diceArr.forEach(setBoard);
setSum(getResult);
});
Build Public Inputs And Witness
Public inputs and witness allow three data types: i64
, bytes
, and bytes-packed
. Each data needs to end with a :
and type.
The data of i64
is read at one time through wasm_input
in zkmain
.
Add task Through zkc-sdk
First, you need to install zkc-sdk
:
npm i zkc-sdk
# or
yarn add zkc-sdk
# or
pnpm add zkc-sdk
Then, create a task object:
const info = {
user_address: userAddress.toLowerCase(),
md5,
public_inputs: publicInputs,
private_inputs: witness,
};
user_address
is the wallet address of the signed user, which needs to be in full lowercase; md5
is the ID of the application, which requires all uppercase; public_inputs
and private_inputs
are arrays.
Call the static method of SDK to convert the task to a string:
const msgHexString = ZKCWasmServiceUtil.createProvingSignMessage(info);
Create a digital signature:
const signature = await withZKCWeb3MetaMaskProvider(
provider =>
(provider as ZKCWeb3MetaMaskProvider).sign(msgHexString) as string,
);
Submit task to zk proof through SDK:
const endpoint = new ZKCWasmServiceHelper(zkcWasmServiceHelperBaseURI);
const res = await endpoint.addProvingTask({ ...info, signature });
Note: Each time you add a task, the program deducts a certain amount of balance from the upload account of the application.
To run and deploy
This is a very typical nextjs based web application despite the wasm part.
npm run start
You will be able to check locally.
You can also deploy them with docker, to other platform like, AWS, GCP, etc.
Typical workflow
First, prepare the program and compile the core part(excutes logic of application) in WebAssembly, which can be used in any high-level programming language. This core program should be able to call the host functions provided in the zkcsdk (Zero-Knowledge Cryptography Software Development Kit) to interact with zkServices hosted in the fusion layer. Additionally, the core program should include the necessary logic for verification.
Next, load and run the core program in a web browser, cloud environment, or any other suitable environment. This allows users to play or interact with the core program while it is running in the selected environment.
Last, the part requires the interaction with layer 1 blockchains, dApps utilize the zkcsdk to generate a proof. This proof is then post to an on-chain zkproxy contract, which will initiate a callback for verification, settlement and other behaviors. The process triggers on-chain data modification, involving verification and settlement. The zk proofs required for this operation are automatically generated from the zkWasm prover network, relieving the developer from having to worry about the intricacies of zero-knowledge cryptography. Instead, the developer can focus on achieving on-chain verification and settlement.
How developers utilize ZKC product
In essence, the process can be broken down into two main steps:
-
Development of Core Application Logic: Utilize programming languages such as C, C++, Rust, TypeScript, etc. to write the core logic of the application. This process should be done in conjunction with linking the ZKC libraries for certain ZKServices, such as VRT, State, and Shuffle. Once completed, compile this code into a WASM image.
-
Integration with ZKCSDK API: This step involves two sub-tasks:
-
Deployment of WASM Image: Deploy the compiled WASM image, aggregate the execution trace (witness), and submit this data to the ZKCFusion layer. This process allows for the automatic generation of a ZKProof.
-
Settlement through ZKProxy Contract: Submit the generated ZKProof and any related information to the on-chain ZKProxy contract for settlement. For instance, this would apply in a scenario where a user has played a game and wishes to redeem their rewards or NFTs on-chain.
-