This is the specification for the ZkVM blockchain, a blockchain containing ZkVM transactions.
Nodes participating in a ZkVM blockchain network must implement the data types and perform the procedures described in this document. Specifically:
- Each node maintains a blockchain state.
- A node creating a new ZkVM network performs the start new network procedure.
- A node joining an existing ZkVM network performs the join existing network procedure.
- Each node performs the apply block procedure on the arrival of each new block.
This document does not describe the mechanism for producing blocks from pending transactions, nor for choosing among competing blocks to include in the authoritative chain (a.k.a. consensus).
The state of a ZkVM blockchain is given by the blockchain-state structure. Each node maintains a copy of this structure. As each new block arrives, it is applied to the current state to produce a new, updated state.
The blockchain state contains:
initialheader
: The initial block header (the header with height 1). This never changes.tipheader
: The latest block header.utreexo
: The Utreexo forest.
A block contains:
header
: A block header.txs
: A list of transactions.
The initial block (at height 1) has an empty list of transactions.
A block header contains:
version
: Integer version number, set to 1.height
: Integer block height. Initial block has height 1. Height increases by 1 with each new block.previd
: ID of the preceding block. For the initial block (which has no predecessor), this is an all-zero string of 32 bytes.timestamp_ms
: Integer timestamp of the block in milliseconds since the Unix epoch: 00:00:00 UTC Jan 1, 1970. Each new block must have a time strictly later than the block before it.txroot
: 32-byte Merkle root hash of the transactions in the block.utxoroot
: 32-byte Utreexo forest root of the utxo set after applying all transactions in the block, or all-zero string if the root has not changed since the previous block.ext
: Variable-length byte string to contain future extensions. Empty in version 1.
A block ID is computed from a block header using the transcript mechanism:
T = Transcript("ZkVM.blockheader")
T.append("version", LE64(version))
T.append("height", LE64(height))
T.append("previd", previd)
T.append("timestamp_ms", LE64(timestamp_ms))
T.append("txroot", txroot)
T.append("utxoroot", utxoroot)
T.append("ext", ext)
blockid = T.challenge_bytes("id")
TBD: describe transaction encoding.
Transaction witness hash commits to the raw transaction: the transaction header, utreexo proofs, tx program, signature and a constraint system proof.
T = Transcript("ZkVM.tx_witness_hash")
T.append_message("tx", encoded_tx)
result = T.challenge_bytes("hash") // 32 bytes
Contract ID is hashed as a merkle leaf hash for the Utreexo as follows:
T.append("contract", contract_id)
In the descriptions that follow, the word “verify” means to test whether a condition is true. If it’s false, all pending procedures abort and a failure result is returned.
A node starts here when creating a new blockchain network. Its blockchain state is set to the result of the procedure.
Inputs:
timestamp_ms
, the current time as a number of milliseconds since the Unix epoch: 00:00:00 UTC Jan 1, 1970.utxos
, the starting utxo set that allows bootstrapping the anchors.
Output:
- Blockchain state.
Procedure:
- Make an initial block header
initialheader
fromtimestamp_ms
andutxos
. - Return a blockchain state with its fields set as follows:
initialheader
:initialheader
tipheader
:initialheader
utxos
:utxos
Inputs:
timestamp_ms
, the current time as a number of milliseconds since the Unix epoch: 00:00:00 UTC Jan 1, 1970.utxos
, the initial list of utxo IDs needed to bootstrap ZkVM anchors.
Output:
- A block header.
Procedure:
- Compute txroot from an empty list of transaction ids.
- Create a new Utreexo from
utxos
, normalize and compute the Utreexo rootutxoroot
. - Return a block header with its fields set as follows:
version
: 1height
: 1previd
: all-zero string of 32-bytestimestamp_ms
:timestamp_ms
txroot
:txroot
utxoroot
:utxoroot
ext
: empty
A new node starts here when joining a running network. It must either:
- obtain all historical blocks, applying them one by one to reproduce the latest blockchain state; or
- obtain a recent copy of the blockchain state
state
from a trusted source (e.g., another node that has already validated the full history of the blockchain) and begin applying blocks beginning atstate.tipheader.height+1
.
An obtained (as opposed to computed) blockchain state state
may be partially validated by computing the Utreexo root from state.utxos
and verifying that it equals state.header.utxoroot
. Note the Utreexo state can be validated only at the points when it was normalized, which may not happen at every block.
Validating a block checks it for correctness outside the context of a particular blockchain state.
Additional correctness checks against a particular blockchain state happen during the apply block procedure, of which this is a subroutine.
Inputs:
block
, the block to validate, at height 2 or above.prevheader
, the previous blockheader.
Output:
- list of transaction logs, one for each transaction in block.txs.
Procedure:
- Verify
block.header.version >= prevheader.version
. - If
block.header.version == 1
, verifyblock.header.ext
is empty. - Verify
block.header.height == prevheader.height+1
. - Verify
block.header.previd
equals the block ID ofprevheader
. - Verify
block.header.timestamp_ms > prevheader.timestamp_ms
. - Let
txlogs
andtxids
be the result of executing the transactions in block.txs withblock.header.version
andblock.header.timestamp_ms
. - Compute txroot from
txids
. - Verify
txroot == block.header.txroot
. - Return
txlogs
.
Inputs:
state
, a blockchain state.version
, a version number for the new block. Note that this must be equal to or greater thanstate.tipheader.version
, the version number of the previous block header.timestamp_ms
, a time for the new block as milliseconds since the Unix epoch, 00:00:00 UTC Jan 1, 1970. This must be strictly greater thanstate.tipheader.timestamp_ms
, the timestamp of the previous block header.txs
, a list of transactions.ext
, the contents of the new block’s “extension” field. Note that at this writing, only block version 1 is defined, which requiresext
to be empty.
Output:
- a new block containing
txs
.
Procedure:
- Let
previd
be the block ID ofstate.tipheader
. - Let
txlogs
andtxids
be the result of executing txs withversion
andtimestamp_ms
. - Let
state´
be the result of applying txlogs tostate
. - Let
txids
be the list of transaction IDs of the transactions intxs
, computed from each transaction’s header entry and the corresponding item fromtxlogs
. - Compute txroot from
txids
to producetxroot
. - If
state´.utreexo
has updates count higher than 65536 (2^16
), normalize the Utreexo and compute the Utreexo rooturoot
. Otherwise, seturoot
to all-zero hash. - Let
h
be a block header with its fields set as follows:version
:version
height
:state.tipheader.height+1
previd
:previd
timestamp_ms
:timestamp_ms
txroot
:txroot
utxoroot
:uroot
ext
:ext
- Return a block with header
h
and transactionstxs
.
Note: the threshold of updates count is not enforced by the verifiers and can be adjusted by the network.
Input:
txs
, a list of transactions.version
, a version number for a block.timestamp_ms
, a block timestamp as milliseconds since the Unix epoch, 00:00:00 UTC Jan 1, 1970.
Outputs:
- a list of
(ID, log)
tuples representing the transaction ID and transaction log for each transaction intxs
.
Procedure:
- Let
txresults
be an empty list of tuples. - For each transaction
tx
intxs
:- Verify
tx.locktime_ms <= timestamp_ms
. - If
version == 1
, verifytx.version == 1
. - Execute
tx
to produce transaction logtxlog
. - Compute transaction ID
txid
from the header entry oftx
and fromtxlog
. - Add
(txid, txlog)
totxresults
.
- Verify
- Return
txresults
.
Note that step 2 can be parallelized across txs
.
Applying a block causes a node to replace its blockchain state with the updated state that results.
Inputs:
block
, the block to apply.state
, the current blockchain state.
Output:
- New blockchain state
state′
.
Procedure:
- Let
txlogs
be the result of validatingblock
withprevheader
set tostate.tipheader
. - Let
state′
bestate
. - Let
state′′
be the result of applying txlogs tostate′
. - Set
state′ <- state′′
. - If
block.header.utxoroot
is not all-zero:- Normalize the Utreexo and compute the Utreexo root.
- Verify
block.header.utxoroot == utxoroot
. - Update the
state′
with the new normalized Utreexo instance.
- Set
state′.tipheader <- block.header
. - Return
state′
.
Inputs:
state
, a blockchain state.txlogs
, a list of transaction logs.
Output:
- Updated blockchain state or failure.
Procedure:
- Let
state′
bestate
. - For each
txlog
intxlogs
, in order:- Let
state′′
be the result of applying the txlog tostate′
to producestate′′
. - Set
state′
<-state′′
if transaction log is not rejected.
- Let
- Return
state′
.
Inputs:
txlog
, a transaction log.state
, a blockchain state.
Output:
- New blockchain state
state′
or failure.
Procedure:
- Let
state′
bestate
. - For each input entry or output entry in
txlog
:- If an input entry, perform the Utreexo delete operation with UTXO ID. If it fails, reject the entire transaction log.
- If an output entry, perform the Utreexo insert operation with UTXO ID.
- Return
state′
if the updates did not fail.
Input:
- Ordered list of block transactions.
Output:
- Merkle root hash of the transaction list.
Procedure:
- Create a transcript
T
with labelZkVM.txroot
. - For each transaction, compute witness hash
w
. - Return
MerkleHash(T, {w})
hashing the witness hash withT.append_message("txwit", w)
.