zkMove Documentation
Welcome to the official documentation for zkMove — a zero-knowledge virtual machine (zkVM) for the Move language. zkMove enables developers to generate succinct zero-knowledge proofs for Move programs, unlocking programmable privacy and verifiable computation on-chain.
Set Up the Development Environment
This guide walks you through installing the CLI tools required to develop with zkMove.
1. Install the zkmove CLI
The zkmove CLI is the primary tool for zkMove development. It supports proof generation, proof verification, and circuit debugging.
cargo install --git https://github.com/zkmove/zkmove.git --branch main zkmove-cli
2. Install the Customized aptos or sui CLI
A customized build of the Aptos or Sui CLI is required. It includes native functions used by the Halo2 on-chain verifier, and is used to interact with the local DevNet, publish contracts, and submit transactions.
2.1 Install aptos CLI
-
Download the release from: https://github.com/zkmove/aptos-core/releases/download/aptos-cli-v7.11.1-zkmove
On macOS, the file is named
aptos-cli-<version>-macOS-arm64.zip. Choose the correct architecture (x86_64orarm64). -
Extract the archive and move the binary to your preferred location.
-
Make it executable:
chmod +x ~/aptos
- Verify the installation:
~/aptos help
2.2 Install sui CLI
Install the zkMove Sui CLI with the following command. An upstream Sui release
binary is not sufficient for the Sui verifier flow because it does not include
the sui::halo2_kzg native verifier module used by verifier_api.
cargo install --git https://github.com/zkmove/sui.git --branch main sui --locked
cargo install places the sui binary under ~/.cargo/bin. Add that
directory to your PATH so the Sui deployment and verification guides can call
sui directly:
export PATH="$HOME/.cargo/bin:$PATH"
To make this persistent for new terminal sessions, add the same line to your
shell profile. For macOS with zsh:
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
Verify that the customized sui CLI is available:
command -v sui
sui --version
3. Clone the halo2-verifier.move Repository
The halo2-verifier.move repository contains the source code for the on-chain Halo2 verifier. You will need it to publish the verifier contracts.
git clone git@github.com:zkmove/halo2-verifier.move.git
Circuit and Proof
Circuit Example
zkMove is designed to be developer-friendly and fits naturally into existing Move development workflows. By leveraging the standard Move package structure, zkMove lets you define zk circuits alongside your Move code with minimal configuration changes.
The example below is a Move module that computes the Fibonacci sequence. The full source can be found in the example directory of the halo2-verifier.move repository. We will build a zk circuit for the test_fibonacci entry function.
// fibonacci.move
module 0x1::fibonacci {
public entry fun test_fibonacci(n: u64) {
let value1 = 0u256;
let value2 = 1u256;
let fibo = 0u256;
let i = 0u64;
while (i < n) {
fibo = value1 + value2;
value1 = value2;
value2 = fibo;
i = i + 1;
};
fibo;
}
}
To define a circuit for this function, add a [circuit.<name>] section to the package manifest Move.toml. In this example, the circuit is named fibonacci:
[package]
name = "example"
version = "0.0.1"
[dependencies]
MoveStdlib = { git = "https://github.com/zkmove/aptos-core.git", subdir = "third_party/move/move-stdlib", rev = "witnessing" }
[addresses]
std = "0x1"
[circuit.fibonacci]
max_execution_rows = 278 # Max rows for the execution subcircuit.
max_poseidon_rows = 100 # Max rows for the Poseidon subcircuit.
entry = { module_id = "0x1::fibonacci", function_name = "test_fibonacci" }
Generate a Witness
First, build the example package. You can use zkmove vm compile, which is
equivalent to move build. The zkmove vm commands read the compiled package
from the Move build output.
# Run from the package root (the directory containing `Move.toml`).
zkmove vm compile --package-path ./ --skip-fetch-latest-git-deps
Then execute the entry function with zkmove vm run to generate the witness.
Specify the entry function with --module-id and --function-name. By
default, witness files are written to the witnesses/ directory:
zkmove vm run \
--package-path ./ \
--module-id 0x1::fibonacci \
--function-name test_fibonacci \
--args 10u64
# Reuse the witness generated by this run in the following commands.
export WITNESS="$(find witnesses -type f -name 'test_fibonacci-*.json' -print | sort | tail -n 1)"
test -f "$WITNESS"
Setup Circuit Artifacts
Before proving, generate the circuit artifacts for this circuit. Use
--circuit-name to select the [circuit.<name>] section in Move.toml. The
command writes params.bin, pk.bin, vk.bin, and metadata.json to
setup/ by default.
There are two ways to set up the circuit:
Option 1: Setup from the entry function
By default, setup builds the circuit directly from the entry function
declared in the [circuit.<name>] section of Move.toml, sizing it according
to max_execution_rows and max_poseidon_rows:
zkmove vm setup \
--package-path ./ \
--circuit-name fibonacci \
--params-path params/kzg_bn254_12.srs
Option 2: Setup from a witness
Alternatively, pass a witness file with --witness. The circuit is then built
and sized from the actual execution trace. The witness must be generated from
the same entry function declared in the circuit section, otherwise the command
fails.
The advantage of this approach is that developers do not need to set
max_execution_rows and max_poseidon_rows in Move.toml—the circuit
dimensions are derived automatically from the execution trace. For beginners,
choosing reasonable values for max_execution_rows and max_poseidon_rows
can be quite difficult, so this approach is easier to start with.
However, it also has a limitation: it is only suitable for code with a fixed execution path (no branches or loops). If the execution path varies with the input arguments, a circuit sized from one particular run may not fit other runs, in which case you should use Option 1 with explicitly configured row limits.
zkmove vm setup \
--package-path ./ \
--circuit-name fibonacci \
--params-path params/kzg_bn254_12.srs \
--witness "$WITNESS"
Public inputs
With either option, if your circuit exposes one or more entry arguments as
public inputs, add the public-input indices with --pubs-indices. For
example:
zkmove vm setup \
--package-path ./ \
--circuit-name fibonacci \
--params-path params/kzg_bn254_12.srs \
--witness "$WITNESS" \
--pubs-indices 0
Use the same --pubs-indices values later when building on-chain verifier
artifacts for this circuit.
The setup step records the circuit’s entry function and configuration in
setup/metadata.json, so the prove and verify commands below no longer
need --circuit-name.
Generate a Proof
Run the following command from the package root. prove runs the entry
function recorded in the setup metadata with the given arguments to generate
the witness, then produces the proof. Proof artifacts are written to the
proofs/ directory by default:
zkmove vm prove \
--package-path ./ \
--args 10u64
# Locate the proof artifacts generated by this run.
export RUN_ID="$(basename "$(find proofs -type f -name 'test_fibonacci-*.proof' -print | sort | tail -n 1)" .proof)"
export PUBS_PATH="proofs/${RUN_ID}.instance"
export PROOF_PATH="proofs/${RUN_ID}.proof"
Optional: verify locally before submitting on-chain.
zkmove vm verify \
--package-path ./ \
--pubs-path "$PUBS_PATH" \
--proof-path "$PROOF_PATH"
prove and verify read setup/metadata.json, setup/params.bin,
setup/pk.bin, and setup/vk.bin by default. Use --setup-dir <dir> if you
store those setup artifacts elsewhere.
Deploy an On-Chain Verifier
This guide deploys a verifier contract to a local Aptos DevNet for the Fibonacci circuit (example/fibonacci).
1. Start the Local DevNet
Start a local Aptos network by following the official guide: https://aptos.dev/network/nodes/localnet/local-development-network#starting-a-local-network
2. Create Account Profiles
Three separate accounts are needed:
| Profile | Purpose |
|---|---|
<contracts-profile> | Publish shared verifier contracts |
<params-profile> | Publish KZG parameters |
<verifier-profile> | Publish per-circuit verifying key and circuit data |
Separating
<params-profile>from<verifier-profile>allows multiple circuits to share the same KZG parameters while each having its own verifier.
From the root of the halo2-verifier.move repository, run the following commands to create the profiles:
aptos init --profile <contracts-profile> --network local
aptos init --profile <params-profile> --network local
aptos init --profile <verifier-profile> --network local
Profiles are saved to .aptos/config.yaml. To check an account address:
aptos config show-profiles --profile <contracts-profile>
Fund each account via the faucet:
aptos account fund-with-faucet --url http://127.0.0.1:8080 --amount 5000000000000000000 --profile <contracts-profile>
aptos account fund-with-faucet --url http://127.0.0.1:8080 --amount 5000000000000000000 --profile <params-profile>
aptos account fund-with-faucet --url http://127.0.0.1:8080 --amount 5000000000000000000 --profile <verifier-profile>
3. Publish Verifier Contracts
Run the following script from the repository root to publish the shared verifier contracts:
PROFILE=<contracts-profile> ./publish_contracts.sh
The Aptos CLI asks for confirmation before each package publish. Review the
transaction details and enter yes for all three publishes.
4. Deploy the Circuit Verifier
Two verifier variants are available:
| Variant | Description |
|---|---|
| Native | Uses native functions for faster verification. |
| Pure Move | Implements verification entirely in Move; better portability. |
Option A — Native Halo2 Verifier (Recommended)
Select the witness from the proof run. The commands below use its filename stem for the generated transaction files:
export WITNESS="$(find example/witnesses -type f -name 'test_fibonacci-*.json' -print | sort | tail -n 1)"
test -f "$WITNESS"
export RUN_ID="$(basename "$WITNESS" .json)"
Step 1. Publish the KZG parameters:
zkmove aptos build-publish-params-native-aptos-txn \
--params-path example/params/kzg_bn254_12.srs \
--params-contract-address <address-of-contracts-profile>
Submit the generated transaction to publish the KZG SRS under <params-profile>:
aptos move run --json-file kzg_bn254_12-publish-params-native.txn --profile <params-profile>
aptos move run also asks for confirmation. Review the transaction and enter
yes before it is submitted; the same applies to every submission below.
Step 2. Build and publish the verifying key and circuit data under <verifier-profile>:
# `-p` specifies the path to the circuit package (must contain a Move.toml file).
zkmove aptos build-publish-circuit-native-aptos-txn \
--params-path example/params/kzg_bn254_12.srs \
-p example \
--circuit-name fibonacci \
-w "$WITNESS" \
--native-verifier-contract-address <address-of-contracts-profile>
If the proof setup used public inputs, pass the same public-input indices here,
for example --pubs-indices 0 1.
This generates two transaction files:
${RUN_ID}-publish-vk-native.txn${RUN_ID}-publish-circuit-native.txn
Submit them in order:
aptos move run --json-file "${RUN_ID}-publish-vk-native.txn" --profile <verifier-profile>
aptos move run --json-file "${RUN_ID}-publish-circuit-native.txn" --profile <verifier-profile>
Option B — Pure Move Verifier (Optional)
Step 1. Build and publish the KZG parameters:
zkmove aptos build-publish-params-aptos-txn \
--params-path example/params/kzg_bn254_12.srs \
--params-contract-address <address-of-contracts-profile>
Submit the generated transaction:
aptos move run --json-file kzg_bn254_12-publish-params.txn --profile <params-profile>
Step 2. Build and publish the circuit:
zkmove aptos build-publish-circuit-aptos-txn \
--params-path example/params/kzg_bn254_12.srs \
-p ./example \
--circuit-name fibonacci \
-w "$WITNESS" \
--verifier-contract-address <address-of-contracts-profile>
If the proof setup used public inputs, pass the same public-input indices here,
for example --pubs-indices 0 1.
Submit the generated transaction:
aptos move run --json-file "${RUN_ID}-publish-circuit.txn" --profile <verifier-profile>
Verify a Proof On-Chain
This guide submits a proof-verification transaction to the local DevNet for the Fibonacci circuit.
Prerequisites: You have already generated a proof using the zkmove CLI.
Select its output files before building a transaction:
export PROOF_PATH="$(find example/proofs -type f -name 'test_fibonacci-*.proof' -print | sort | tail -n 1)"
test -f "$PROOF_PATH"
export RUN_ID="$(basename "$PROOF_PATH" .proof)"
export PUBS_PATH="example/proofs/${RUN_ID}.instance"
test -f "$PUBS_PATH"
Option A — Native Halo2 Verifier
Step 1. Build the verify-proof transaction:
export K_VALUE=$(jq -r .k example/setup/metadata.json)
zkmove aptos build-verify-proof-native-txn \
--pubs-path "$PUBS_PATH" \
--proof-path "$PROOF_PATH" \
--k $K_VALUE \
--native-verifier-contract-address <address-of-contracts-profile> \
--params-address <address-of-params-profile> \
--native-verifier-address <address-of-verifier-profile>
If jq is not available, replace $K_VALUE with the k value recorded in
example/setup/metadata.json.
Step 2. Submit the transaction. Any account can submit the verification:
aptos move run --json-file "${RUN_ID}-verify-proof-native.txn" --profile <any-profile>
Option B — Pure Move Verifier
Step 1. Build the verify-proof transaction:
zkmove aptos build-verify-proof-aptos-txn \
--pubs-path "$PUBS_PATH" \
--proof-path "$PROOF_PATH" \
--verifier-contract-address <address-of-contracts-profile> \
--params-address <address-of-params-profile> \
--verifier-address <address-of-verifier-profile>
Step 2. Submit the transaction:
aptos move run --json-file "${RUN_ID}-verify-proof.txn" --profile <any-profile>
Deploy an On-Chain Verifier on Sui
This guide deploys a verifier contract to a local Sui network (localnet) for the Fibonacci circuit (example/fibonacci).
| Object | Purpose |
|---|---|
SerializedParams | Serialized KZG verifier parameters |
SerializedVK | Serialized Halo2 verifying key plus the matching zkMove circuit metadata |
The circuit metadata is uploaded through a separate builder, but it is finalized together with the verifying key into one SerializedVK object. Verification later needs only PARAMS_OBJECT_ID and VK_OBJECT_ID.
1. Start localnet
Start a local Sui network with the customized sui CLI:
sui start \
--force-regenesis \
--fullnode-rpc-port 9000 \
--with-faucet=127.0.0.1:9123
In another terminal, configure the client:
sui client new-env --alias localnet --rpc http://127.0.0.1:9000
sui client switch --env localnet
sui client new-address ed25519 zkmove-local
sui client switch --address zkmove-local
sui client faucet --address zkmove-local --url http://127.0.0.1:9123/gas
sui client balance
2. Publish the Verifier API Package
The Sui verifier API package lives in the halo2-verifier.move repository:
halo2-verifier.move/packages/api-sui
Publish it to localnet. Use the localnet client environment for the target
chain, but build with the package’s existing testnet build environment:
sui client switch --env localnet
# Path to the local verifier API Move package.
export VERIFIER_API_PACKAGE_DIR=/path/to/halo2-verifier.move/packages/api-sui
sui client --json -q test-publish \
--build-env testnet \
--skip-dependency-verification \
--gas-budget 1000000000 \
"$VERIFIER_API_PACKAGE_DIR"
Save the published package ID from the published object change of STDOUT:
export VERIFIER_API_PACKAGE=<published-package-id>
3. Build the Verifier Artifact Bytes
Use the Sui native transaction builders to produce the serialized byte blobs
needed by the on-chain verifier. In this step, the generated JSON files are
only used as artifact containers; do not submit these large pure-argument calls
directly if the artifacts exceed Sui’s argument size limit. Step 4 uploads the
same bytes through the chunked artifact_builder flow.
Run from the halo2-verifier.move repository root:
export WITNESS="$(find example/witnesses -type f -name 'test_fibonacci-*.json' -print | sort | tail -n 1)"
test -f "$WITNESS"
export RUN_ID="$(basename "$WITNESS" .json)"
export ARTIFACTS_DIR="txns/sui-artifacts/${RUN_ID}"
mkdir -p "$ARTIFACTS_DIR"
zkmove sui build-publish-params-native-txn \
--params-path example/params/kzg_bn254_12.srs \
--verifier-api-package $VERIFIER_API_PACKAGE \
--output-dir "$ARTIFACTS_DIR"
zkmove sui build-publish-circuit-native-txn \
--params-path example/params/kzg_bn254_12.srs \
-p example \
--circuit-name fibonacci \
-w "$WITNESS" \
--verifier-api-package $VERIFIER_API_PACKAGE \
--output-dir "$ARTIFACTS_DIR"
Pass the verifier API package published in Step 2 so the generated Sui
move-call descriptors point at the same API package used later by proof
verification. The params-store arguments are omitted here because this guide
only extracts the serialized byte arrays from the generated JSON files; the
chunked upload in Step 4 uses the builder objects created on your Sui network.
If the proof setup used public inputs, pass the same public-input indices to
build-publish-circuit-native-txn, for example --pubs-indices 0 1.
This produces JSON files whose Sui move-call arguments contain the artifact bytes:
$ARTIFACTS_DIR/kzg_bn254_12-publish-params-native.txn$ARTIFACTS_DIR/${RUN_ID}-publish-vk-native.txn
Step 4 reads these descriptor files directly. Replace the witness filename with the witness generated for your own circuit.
4. Upload the Artifacts as Sui Objects
Sui limits the size of pure vector<u8> arguments. Use the verifier API’s
artifact_builder module to upload large artifacts in chunks. The repository
provides a wrapper script for the full flow. Run from the halo2-verifier.move
repository root:
scripts/upload_sui_artifacts.sh \
--verifier-api-package "$VERIFIER_API_PACKAGE" \
--artifacts-dir "$ARTIFACTS_DIR" \
--out-dir "txns/sui-artifacts-upload/${RUN_ID}"
After the script finishes, it prints the finalized object IDs and writes them to
txns/sui-artifacts-upload/${RUN_ID}/sui-artifact-objects.env:
PARAMS_OBJECT_ID=<serialized-params-object-id>
VK_OBJECT_ID=<serialized-vk-object-id>
Load them into your current shell:
source "txns/sui-artifacts-upload/${RUN_ID}/sui-artifact-objects.env"
PARAMS_OBJECT_ID is a SerializedParams object. VK_OBJECT_ID is a
SerializedVK object that bundles both the Halo2 verifying key and the matching
zkMove circuit metadata. These object IDs are the Sui equivalent of the Aptos
params/verifier addresses used in the Aptos guide.
Verify a Proof On-Chain on Sui
This guide builds and submits a proof-verification call to a local Sui network (localnet).
The Sui path currently uses the native Halo2 KZG verifier included in the customized sui CLI. The zkmove CLI builds a Sui move-call descriptor, and sui client call submits it.
Prerequisites:
- You have generated a proof with
zkmove vm. - You have deployed the Sui verifier API package.
- You have published the verifier artifacts and saved:
VERIFIER_API_PACKAGEPARAMS_OBJECT_IDVK_OBJECT_ID
Select the proof artifacts from the current proof run before continuing:
export PROOF_PATH="$(find example/proofs -type f -name 'test_fibonacci-*.proof' -print | sort | tail -n 1)"
test -f "$PROOF_PATH"
export RUN_ID="$(basename "$PROOF_PATH" .proof)"
export PUBS_PATH="example/proofs/${RUN_ID}.instance"
test -f "$PUBS_PATH"
Install jq
This guide uses jq to inspect JSON transaction outputs from sui client --json.
Install it before submitting the verification call.
On macOS:
brew install jq
On Ubuntu or Debian:
sudo apt-get update
sudo apt-get install jq
Verify the installation:
jq --version
1. Build the Verify-Proof Data
Run from the halo2-verifier.move repository root. Replace the file names and object IDs with the values from your circuit and deployment:
export K_VALUE=$(jq -r .k example/setup/metadata.json)
export VERIFY_DIR="txns/sui-verify/${RUN_ID}"
mkdir -p "$VERIFY_DIR"
zkmove sui build-verify-proof-native-txn \
--pubs-path "$PUBS_PATH" \
--proof-path "$PROOF_PATH" \
--verifier-api-package $VERIFIER_API_PACKAGE \
--params-object-id $PARAMS_OBJECT_ID \
--vk-object-id $VK_OBJECT_ID \
--k $K_VALUE \
--output "$VERIFY_DIR"
The command writes a file like:
$VERIFY_DIR/${RUN_ID}-verify-proof-native.txn
The file contains a JSON move-call descriptor with package, module,
function, args, and cli_args. For Sui, use this file as a data container:
the proof may be larger than Sui’s 16 KiB pure-argument limit, so do not submit
this generated native_verifier::verify call directly unless you know the
proof argument is small enough.
2. Upload the Proof in Chunks
Sui limits the size of pure vector<u8> arguments. Use the verifier API’s
artifact_builder module to upload the proof bytes in chunks. The
halo2-verifier.move repository provides a wrapper script for this flow. Run
from the halo2-verifier.move repository root:
scripts/upload_sui_proof.sh \
--verifier-api-package "$VERIFIER_API_PACKAGE" \
--verify-txn "$VERIFY_DIR/${RUN_ID}-verify-proof-native.txn" \
--out-dir "txns/sui-proof-upload/${RUN_ID}"
After the script finishes, it prints PROOF_BUILDER and PROOF_DIGEST, and
writes all values needed by Step 3 to
txns/sui-proof-upload/${RUN_ID}/sui-proof-builder.env.
3. Submit the Verification
Load the proof-builder values into your current shell:
source "txns/sui-proof-upload/${RUN_ID}/sui-proof-builder.env"
Call artifact_builder::verify_proof_builder. This consumes the proof builder,
checks its digest, and verifies the proof without passing the whole proof as one
pure argument:
sui client --json -q call \
--package $VERIFIER_API_PACKAGE \
--module artifact_builder \
--function verify_proof_builder \
--gas-budget 1000000000 \
--args \
$PARAMS_OBJECT_ID \
$VK_OBJECT_ID \
$PROOF_BUILDER \
"$PROOF_DIGEST" \
"$PUBLIC_INPUTS_JSON" \
$KZG_VARIANT \
$K_PRESENT \
$K_VALUE \
> "$VERIFY_DIR/verify-result.json"
Any funded localnet account can submit the verification call.
Check that the transaction succeeded:
jq '.effects.status' "$VERIFY_DIR/verify-result.json"
A valid proof returns a successful transaction status. An invalid proof aborts
inside verifier_api::artifact_builder::verify_proof_builder.
If jq reports a parse error, inspect the file directly:
cat "$VERIFY_DIR/verify-result.json"
That usually means sui client call wrote a plain-text execution error instead
of JSON. An abort with code 6 from artifact_builder::verify_proof_builder
means the proof digest matched and the native verifier returned false. For
proofs generated from a downsized circuit, first check that the generated verify
txn has .args[5] == true and .args[6] set to the same k used when proving.
zkMove: Programmable Privacy for Move Smart Contracts
zkMove Team
contact@zkmove.net
April 2026
Abstract
As performance bottlenecks in smart contract platforms continue to ease, privacy has emerged as the next critical challenge. The transparent nature of blockchains inherently exposes users’ behavioral patterns, intentions, and asset holdings.
This concern becomes even more acute as blockchains and smart contracts are positioned as the trustworthy coordination layer for the Agentic Economy. Hidden vulnerabilities in applications become easier for attackers to discover and exploit, while AI agents themselves introduce entirely new attack surfaces.
zkMove is a secure, high-performance zero-knowledge virtual machine (zkVM) purpose-built for the Move programming language. It empowers Move smart contracts to access and process private data in a fully programmable and trustless manner.
From a product perspective, zkMove is both a middleware and an SDK — one that can be integrated into any Move blockchain (L1 or L2), allowing Move developers to build privacy-preserving decentralized applications without deep cryptographic expertise.
This litepaper presents:
- The motivation for introducing programmable privacy to the Move ecosystem
- How to build a privacy-focused zkVM for Move
- zkMove’s ASIC-inspired circuit architecture and its hybrid on-chain/off-chain computation model
- Core circuit design, including instruction loading, function-scoped execution, memory consistency, and support for Move’s unique runtime type system
- Performance benchmarks of zkMove v0.5, highlighting improvements in proving time and proof size
- Representative use cases such as confidential assets and incomplete-information games
Acknowledgements
We are grateful to Shisheng Li, Ryan Fang, Star Li, Tim Yang, and Xiaofeng Li for their valuable advice and support throughout the development of this project.
Introduction
The Evolution of Smart Contract Platforms
The evolution of smart contract platforms has followed two distinct paths. The first is the Layer 2 (L2) scaling roadmap pioneered by Ethereum and its ecosystem. The second is the horizontal scaling approach taken by high-performance Layer 1 blockchains such as Solana, Sui, and Aptos. Both paths share the same core objective: dramatically improving performance and throughput.
The horizontal scaling approach has progressed through three major stages:
| Period | Representative | Key Characteristics |
|---|---|---|
| 2015–2018 | Ethereum | First Turing-complete smart contract platform |
| 2018–2022 | Solana | Parallel execution, high throughput (TPS) |
| 2022–2026 | Sui / Aptos | Extreme parallelism, high TPS, enhanced security |
To date, both approaches have largely delivered on their respective goals, notwithstanding the degree of decentralization sacrificed by L2 solutions.
Privacy as the Next Priority
With performance largely addressed, privacy has emerged as the next critical challenge. The inherently public nature of blockchains exposes users’ behavioral patterns, intents, and asset holdings — creating persistent surveillance and attack vectors for malicious actors.
This concern becomes even more acute as blockchains and smart contracts are increasingly positioned as the trustworthy coordination layer for the Agentic Economy. On one hand, hidden vulnerabilities in applications become easier for attackers to discover and exploit at scale. On the other hand, AI agents themselves introduce entirely new attack surfaces.
In Liu Cixin’s The Dark Forest, the safest and most rational strategy in a universe that may harbor advanced civilizations is simple: do not reveal your position. Without robust privacy protections, blockchains risk becoming precisely such a “dark forest”. Existing smart contract platforms must close this privacy gap to become a truly secure and trustworthy coordination layer for the Agentic Economy.
Zero-Knowledge Proofs and zkVMs
Zero-knowledge proofs (ZKPs, or simply ZK) are a cryptographic primitive that allow a prover to convince a verifier that a computation was performed correctly, without revealing any information about the underlying inputs. As a rapidly evolving area of cryptography, ZK is seeing growing adoption across blockchain scaling, privacy protection, and verifiable computation.
Despite their potential, ZKPs have historically suffered from poor programmability. Building a ZKP application typically requires cryptography experts to hand-craft arithmetic circuits — a time-consuming process that has hindered mainstream adoption.
A zero-knowledge virtual machine (zkVM) addresses this problem directly. With a zkVM, developers write ordinary code and the system automatically generates efficient proofs, dramatically lowering the barrier to building ZKP-powered applications.
Why Move?
The Move programming language was originally developed by Meta (Facebook) for writing smart contracts on the Libra blockchain, and has since been adopted by Sui and Aptos. Move’s most distinctive feature is asset-oriented programming: digital assets are modeled as resources with strict ownership semantics — they cannot be copied, accidentally discarded, or double-spent, and must be explicitly transferred or destroyed.
Move is the natural choice for a privacy-focused zkVM for two reasons. First, The essence of blockchain is a value network, and its core purpose is to enable digital assets to move more efficiently. Second, Sui and Aptos represent the most important direction in the evolution of smart contract platforms — and building for Move means building where the ecosystem is headed.
What is zkMove?
Technically, zkMove is a secure, high-performance zkVM designed specifically for the Move programming language. It empowers Move smart contracts to access and process private data in a fully programmable and trustless manner.
From a product perspective, zkMove is both a middleware and an SDK — one that Move developers can use to build privacy-preserving decentralized applications without deep cryptographic expertise.
zkMove’s path to success hinges on two key challenges. First, the ZK space is evolving rapidly; staying competitive requires continuously advancing circuit design while keeping pace with improvements in the underlying proof systems. Second, whether Move’s ecosystem can secure a meaningful role in the emerging Agentic Economy — and whether zkMove can deliver unique value within that ecosystem — are questions that deserve serious consideration.
How to build a zkVM for Move
The Move Virtual Machine
Move is designed to be cross-platform. Programs are compiled to bytecode and executed by the Move Virtual Machine (MoveVM).
MoveVM is a stack-based bytecode virtual machine composed of four main components:
- Interpreter — executes bytecode instructions sequentially
- Operand stack — holds intermediate computation values
- Local variable storage — stores function-local variables
- Global state — persistent on-chain storage
For simplicity, we refer to the stack, local variables, and global state collectively as memory.
The Move instruction set covers a broad range of operations:
- Stack push and pop
- Load and store of local variables
- Arithmetic and logical operations
- Type casting
- Control flow (branches, jumps)
- Function calls and returns
- Struct and vector operations
- Global state read/write
- Exception handling
Each instruction reads values from memory, applies the defined semantics, and writes the result back to memory.
How to Build a zkVM for Move
There are three main approaches to building a zkVM for Move, each with distinct trade-offs.
Approach 1: Run MoveVM on a RISC-V zkVM
Since MoveVM is implemented in Rust, its source code can be compiled to RISC-V. A RISC-V zkVM (such as RISC Zero or SP1) can then prove the correct execution of the resulting RISC-V binary.
- Advantage: No need to build custom circuits for MoveVM; most of the toolchain already exists.
- Disadvantage: The entire MoveVM is effectively “run inside” another virtual machine, resulting in a long execution path and poor performance.
Approach 2: Compiling Move Bytecode to RISC-V via move-llvm
Move bytecode can be compiled to RISC-V instructions using the move-llvm backend, bypassing the MoveVM interpreter entirely.
- Advantage: Better performance than Approach 1.
- Disadvantage: MoveVM and the Move language are tightly coupled — the bytecode relies on runtime safety checks that cannot be replicated in RISC-V. Compiling bytecode to RISC-V breaks these runtime guarantees. Proposed workarounds (e.g., having a trusted party compile Move bytecode to LLVM IR) compromise decentralization. [1]
Approach 3: Build a Dedicated Circuit for MoveVM (zkMove’s Approach)
zkMove takes the third approach: building a custom circuit that directly verifies the execution of Move bytecode against the MoveVM semantics.
- Advantage: Achieves the best performance without sacrificing security. Because Move bytecode is executed directly on the zkVM, we can exploit the program’s code structure for further optimizations.
- Disadvantage: Building a full circuit for MoveVM is a complex and time-intensive engineering effort.
Type safety and memory safety. Move’s static type system and linear resource semantics eliminate entire classes of vulnerabilities at compile time — such as use-after-free errors, resource duplication, and accidental destruction. Its strict ownership rules and module encapsulation make classic reentrancy attacks extremely difficult or impossible in most cases.
Architecture
Mainstream zkVMs: A Two-Stage General-Purpose Architecture
Mainstream zkVMs such as RISC Zero[4] and Succinct SP1[5], both based on the RISC-V instruction set, are primarily designed for general-purpose computation. They are widely used for blockchain scaling, off-chain co-processors, and other verifiable computing applications. These zkVMs typically adopt a two-stage circuit architecture to balance proving efficiency with on-chain verification cost:
Stage 1: RISC-V Execution Circuit → STARK Proof
- Proves correct program execution on the RISC-V virtual machine using the zk-STARK protocol.
- Strengths: Transparent setup (no trusted setup required), post-quantum secure, relatively efficient proof generation.
- Weakness: Produces large proof sizes (typically hundreds of kilobytes to several megabytes).
Stage 2: Compression / Recursion Circuit → Groth16 SNARK
- Aggregates and compresses the Stage 1 STARK proof into a Groth16 SNARK (over the BN254 curve).
- Produces a constant-size proof suitable for on-chain verification with low gas cost.
zkMove: An ASIC-Inspired Architecture
Single-Stage
zkMove draws inspiration from ASIC (Application-Specific Integrated Circuit) design philosophy. Rather than targeting universal computation, zkMove generates a dedicated circuit and verification key for each application. The circuit is tailored to the specific set of opcodes that application uses.
Compared to the two-stage architecture of mainstream zkVMs, this approach yields several key benefits:
- Minimal proof size — typically only tens of kilobytes, significantly smaller than the uncompressed STARK proofs from general-purpose zkVMs.
- No compression stage required — the final proof can be verified on-chain directly, eliminating the need for recursive aggregation or a secondary SNARK wrapper.
On-Chain / Off-Chain Hybrid Computation
For complex computations, zkMove’s dedicated circuit can grow nearly as large as a general-purpose zkVM circuit, diminishing the benefits of its ASIC-like design.
To address this, zkMove introduces an on-chain / off-chain hybrid computation model. Developers separate privacy-sensitive state and logic from the main smart contract and define them as one or more off-chain functions. These functions execute on the user’s client and generate zero-knowledge proofs; only the proofs are submitted on-chain. The on-chain contract then verifies the submitted proofs and executes the remaining logic.
This hybrid approach delivers the best of both worlds:
- On-chain transparency and security — core contract logic remains fully public and verifiable on-chain, eliminating the need to generate zero-knowledge proofs for it.
- Off-chain privacy — sensitive data and computations never leave the user’s client.
Strengths and Trade-offs
Strengths of zkMove
- Client-side proving — User inputs and sensitive data remain private on the client and are never exposed to third parties.
- Instant finality — Proofs are verified directly on-chain with instant finality.
- Full decentralization and trustlessness — zkMove inherits the same security guarantees as the underlying L1 blockchain.
- Seamless tooling compatibility — Fully compatible with existing Move development tools; current Move programs can run with only minimal modifications.
Trade-offs
- Not suitable for highly complex off-chain computations.
- Each application requires its own dedicated verification key, which increases deployment and key management overhead compared to general-purpose zkVMs.
Summary: Mainstream zkVMs excel at general-purpose computation and are well-suited for L2 scaling. zkMove is purpose-built for privacy-preserving computation, making it an ideal choice for programmable privacy on L1.
Circuit Design
Core Requirements of Any zkVM Circuit
Regardless of architecture, every zkVM circuit must ensure three fundamental correctness properties:
- Correct instruction loading — the right instructions are fetched for execution.
- Correct instruction execution — each instruction is executed according to its defined semantics.
- Memory consistency — every value read from memory equals the value most recently written to that location.
The following sections compare how mainstream zkVMs address these requirements and outline zkMove’s design choices.
1. Correct Instruction Loading
RISC Zero
The full RISC-V ELF binary is loaded into initial memory. A Merkle tree is constructed over the memory pages using the Poseidon2 hash function, with the circuit enforcing the Merkle tree’s correctness. Proving overhead scales with the complexity of the Poseidon2 and Merkle tree circuits.
Succinct SP1
Program instructions are loaded directly as the initial memory state of the MemoryLocalChip. This state is exposed as a public trace/table, allowing the circuit to reference instructions with almost zero additional proving overhead. Compared to RISC Zero’s Merkle-tree-based approach, this design significantly reduces circuit complexity and proving cost, but at the expense of program privacy — the bytecode becomes publicly visible.
zkMove
zkMove stores the contract bytecode in a fixed lookup table (a constant/fixed column in the Halo2 circuit). Thanks to the compact nature of Move bytecode, this table has negligible impact on proof size, with instruction-fetching overhead approximately O(1). The fixed table is baked into the circuit, and its commitment is included in the Verification Key (VK) for that specific contract, keeping the program bytecode private.
2. Correct Instruction Execution
Selector-Based Dispatch in Mainstream zkVMs
Mainstream zkVMs employ selector columns to dispatch instruction semantics. For a RISC-V ISA with instructions, each instruction defines a set of semantic constraint polynomials:
where denotes the relevant execution trace columns (e.g., clk, pc, opcode).
Each selector column satisfies:
The combined constraint polynomial across the execution trace is:
This approach evaluates all instruction constraints per row, even though only one is active at any given time.
zkMove: Function-Scoped Circuits
zkMove scopes its circuit to only the opcodes used in the current function. Let denote the number of distinct opcodes in that function. The main constraint polynomial simplifies to:
This design offers two key properties:
- Best case (): A trivial function with only a
retinstruction reduces to , incurring zero dispatch overhead. (Note: This is an idealized case; in practice, even the simplest functions require a small set of basic instructions for function prologue and return handling.) - Worst case (): A function using all opcodes falls back to the standard mainstream form — incurring no additional cost.
In practice, most privacy-sensitive functions use only a small subset of opcodes, making zkMove’s circuit significantly more compact than general-purpose zkVM circuits.
3. Memory Consistency Checking
Early zkVMs relied on sorting-based methods [2] for memory consistency verification. Modern zkVMs, including zkMove, have adopted the shuffle argument instead.
zkMove integrates execution and memory into a single unified chip to minimize circuit size. By applying the address-cycle method [3], memory consistency is verified through a single shuffle operation, reducing inter-chip communication and circuit complexity.
The Unique Challenges of Move
Unlike other smart contract languages, Move enforces runtime type safety. In the MoveVM, all values on the stack and in local variables are typed — in sharp contrast to languages like EVM, where all types collapse to U256 at runtime.
This poses two circuit design challenges:
- How to represent typed values within the circuit.
- How to enforce type checks without substantial performance overhead.
zkMove’s Solution
Complex types are flattened into a list of primitive types, represented as a tuple:
(index, sub_index, value, value_header)
Type checks are enforced only in three scenarios:
- When passing arguments to a function.
- When creating a new value.
- When modifying an existing value.
In all other cases, the Memory Consistency Check (MCC) ensures a value’s type remains consistent across reads and writes. This approach maintains Move’s type safety guarantees without appreciably increasing circuit size.
This page provides a high-level overview of zkMove’s circuit design. For detailed technical specifications, refer to the zkMove Circuit Design Document.
Performance
Overview
zkMove v0.5 delivers significant improvements over v0.4:
- Proving time reduced by 0.5×–3× depending on the workload.
- Proof size reduced to a flat ~25 KB, enabling near-second-level on-chain finality.
Benchmark Results
Test environment: MacBook Pro, Apple M1 Max, 64 GB RAM
Proving Time (seconds):
| Test Case | v0.3 | v0.4 | v0.5 |
|---|---|---|---|
| Fibonacci N = 8 | 33.2 | 3.8 | 0.9 |
| N = 10 | 50.1 | 4.2 | 1.0 |
| N = 20 | 90.9 | 4.4 | 1.6 |
| N = 50 | 162.3 | 4.7 | 2.8 |
| N = 100 | 0.0 | 7.9 | 5.0 |
Proof Size (KB):
| Test Case | v0.3 | v0.4 | v0.5 |
|---|---|---|---|
| Fibonacci N = 1..100 | 450.8 | 43.7 | 24.8 |
Versions:
- v0.3: Initial implementation (execution circuit V1, sorting-based mcc)
- v0.4: First round of optimizations (execution circuit V2, shuffle-based mcc)
- v0.5: Further optimizations (ASIC-inspired execution circuit)
Benchmark Description
For details on the benchmark methodology and test cases, refer to the [benchmark specification].
Use Cases
Privacy-Preserving Smart Contracts
A traditional smart contract operates entirely on public on-chain data and publicly visible contract code. In contrast, a privacy-preserving smart contract can handle both public and private data — and if desired, even the contract code can remain hidden.
This privacy capability is enabled by zkMove’s on-chain / off-chain hybrid computation model. Developers extract privacy-sensitive state and logic from the main smart contract and implement them as off-chain functions. These functions execute on the user’s client and generate zero-knowledge proofs; only the proofs are submitted on-chain. The on-chain contract then verifies the submitted proofs and executes the remaining public logic.
Example: Confidential Assets
Confidential Assets (CA) allow digital assets to be stored and transferred on-chain in encrypted form, visible only to authorized parties. This preserves financial privacy without sacrificing verifiability.
The following example demonstrates a portion of a CA smart contract. It allows a user to prove that their asset balance falls within a given range [min, max], without revealing the actual amount.
On-Chain Contract
The on-chain contract receives a proof from the user and verifies it against the encrypted asset value:
module confidential_asset::on_chain {
use aptos_std::bn254_algebra::Fr;
use halo2_common::public_inputs;
use verifier_api::verifier;
// Error codes
const EINVALID_PROOF: u64 = 100;
const EINVALID_INPUT: u64 = 101;
// KZG variants
const KZG_GWC: u8 = 1;
const KZG_SHPLONK: u8 = 0;
public entry fun range_check(
encrypted_value: u256,
min: u128,
max: u128,
proof: vector<u8>
) {
assert!(min <= max, EINVALID_INPUT);
// Verify: "encrypted_value is an encryption of a value in range [min, max]"
let pi = public_inputs::empty<Fr>(public_inputs::get_vm_public_inputs_column_count());
public_inputs::push_u128(&mut pi, min);
public_inputs::push_u128(&mut pi, max);
public_inputs::push_u256(&mut pi, encrypted_value);
assert!(
verifier::verify_proof(
@param_address,
@circuit_range_check_address,
pi,
proof,
KZG_GWC
),
EINVALID_PROOF
);
}
}
Off-Chain Client Function
The actual asset value is stored on the user’s client. The user executes the following function off-chain to generate a proof that the value lies within [min, max]. Only the encrypted value (a hash) and the proof are sent on-chain — the raw asset amount is never exposed.
module confidential_asset::off_chain {
use std::zkhash;
const E_INVALID_ENCRYPTION: u64 = 0;
const E_INVALID_INPUT: u64 = 1;
// Public inputs: min, max, encrypted_value
public entry fun check_range(
value: u128,
min: u128,
max: u128,
encrypted_value: u256,
nonce: u128
) {
assert!(value >= min && value <= max, E_INVALID_INPUT);
assert!(zkhash::hash(value, nonce) == encrypted_value, E_INVALID_ENCRYPTION);
}
}
The complete Confidential Assets example is available in the zkMove repository.
Example: Incomplete-Information Games
zkMove can also power incomplete-information games — on-chain games where players hold private state that is never revealed to opponents. A well-known example of this pattern is Dark Forest, where zero-knowledge proofs allow players to hide planet locations while still enforcing game rules on a public blockchain.
The on-chain contract verifies proofs of valid moves without ever learning the players’ private coordinates:
module dark_forest::on_chain {
use aptos_std::bn254_algebra::Fr;
use halo2_common::public_inputs;
use verifier_api::verifier;
const E_INVALID_COORDINATES: u64 = 0;
/// Moves a player from position (x1, y1) to (x2, y2), verifying the Euclidean distance
/// using a zero-knowledge proof.
///
/// # Arguments
/// * `hash_1` - Poseidon hash of the player's current position (x1, y1)
/// * `hash_2` - Poseidon hash of the target position (x2, y2)
/// * `distance_squared` - Squared Euclidean distance between the two positions
/// * `proof` - Zero-knowledge proof generated by the Euclidean distance circuit
/// * `kzg_variant` - KZG commitment variant used for proof verification
public entry fun move_to(
hash_1: u256,
hash_2: u256,
distance_squared: u128,
proof: vector<u8>,
kzg_variant: u8
) acquires GameManager {
let pi = public_inputs::empty<Fr>(public_inputs::get_vm_public_inputs_column_count());
public_inputs::push_u256(&mut pi, hash_1);
public_inputs::push_u256(&mut pi, hash_2);
public_inputs::push_u128(&mut pi, distance_squared);
assert!(
verifier::verify_proof(
@param_address,
@circuit_euclid_distance_address,
pi,
proof,
kzg_variant
),
E_INVALID_COORDINATES
);
}
}
The proof is generated by the player’s client using the following off-chain function, which checks that the distance between the current and target positions is correct without revealing any coordinates:
module dark_forest::euclid_distance {
use std::zkhash;
const E_INVALID_COORDINATES: u64 = 0;
/// Euclidean distance squared (no sqrt needed).
/// Public inputs: hash_1, hash_2, distance_squared
public entry fun check_euclid_distance(
x1: u128, y1: u128,
x2: u128, y2: u128,
hash_1: u256, hash_2: u256,
distance_squared: u128
) {
assert!(zkhash::hash(x1, y1) == hash_1, E_INVALID_COORDINATES);
assert!(zkhash::hash(x2, y2) == hash_2, E_INVALID_COORDINATES);
let dx = if (x1 > x2) { x1 - x2 } else { x2 - x1 };
let dy = if (y1 > y2) { y1 - y2 } else { y2 - y1 };
let expected_distance_squared = dx * dx + dy * dy;
assert!(distance_squared == expected_distance_squared, E_INVALID_COORDINATES);
}
}
The complete Dark Forest example is available in the zkMove repository.
Current Limitations
zkMove’s support for programmable privacy is still in its early stages. The current model covers scenarios where off-chain computation depends solely on the user’s own private data combined with public on-chain state.
Scenarios requiring interaction between the private data of multiple users — for example, comparing two private values from different parties — are not yet supported. This is an active area of development.
References
The following works are cited in this litepaper.
1. Brian Anderson
Writing an LLVM backend for the Move language in Rust. https://brson.github.io/2023/03/12/move-on-llvm
2. David Wong
Cairo’s Public Memory. https://www.cryptologie.net/article/603/cairos-public-memory
3. Yibin Yang and David Heath
Two Shuffles Make a RAM: Improved Constant Overhead Zero-Knowledge RAM. 2023. https://eprint.iacr.org/2023/1115
4. RISC Zero
RISC Zero zkVM: Scalable, Transparent Arguments of RISC-V Integrity. 2023. https://dev.risczero.com/proof-system/proof-system-sequence-diagram
5. Succinct SP1
SP1: A high-performance zkVM for RISC-V programs. 2024. https://github.com/succinctlabs/sp1