zkas bincode

The bincode design for zkas is the compiled code in the form of a binary blob, that can be read by a program and fed into the VM.

Our programs consist of four sections: constant, literal, witness, and circuit. Our bincode represents the same. Additionally, there is an optional section called .debug which can hold debug info related to the binary.

We currently keep all variables on one heap, and literals on another heap. Therefore before each HEAP_INDEX we prepend HEAP_TYPE so the VM is able to know which heap it should do lookup from.

The compiled binary blob has the following layout:

MAGIC_BYTES
BINARY_VERSION
K
NAMESPACE
.constant
CONSTANT_TYPE CONSTANT_NAME 
CONSTANT_TYPE CONSTANT_NAME 
...
.literal
LITERAL
LITERAL
...
.witness
WITNESS_TYPE
WITNESS_TYPE
...
.circuit
OPCODE ARG_NUM HEAP_TYPE HEAP_INDEX ... HEAP_TYPE HEAP_INDEX
OPCODE ARG_NUM HEAP_TYPE HEAP_INDEX ... HEAP_TYPE HEAP_INDEX
...
.debug
TBD

Integers in the binary are encoded using variable-integer encoding. See the serial crate and module for our Rust implementation.

Sections

MAGIC_BYTES

The magic bytes are the file signature consisting of four bytes used to identify the zkas binary code. They consist of:

0x0b 0x01 0xb1 0x35

BINARY_VERSION

The binary code also contains the binary version to allow parsing potential different formats in the future.

0x02

K

This is a 32bit unsigned integer that represents the k parameter needed to know how many rows our circuit needs.

NAMESPACE

This sector after MAGIC_BYTES, BINARY_VERSION, and K contains the reference namespace of the code. This is the namespace used in the source code, e.g.:

constant "MyNamespace" { ... }
witness  "MyNamespace" { ... }
circuit  "MyNamespace" { ... }

The string is serialized with variable-integer encoding.

.constant

The constants in the .constant section are declared with their type and name, so that the VM knows how to search for the builtin constant and add it to the heap.

.literal

The literals in the .literal section are currently unsigned integers that get parsed into a u64 type inside the VM. In the future this could be extended with signed integers, and strings.

.witness

The .witness section holds the circuit witness values in the form of WITNESS_TYPE. Their heap index is incremented for each witness as they're kept in order like in the source file. The witnesses that are of the same type as the circuit itself (typically Base) will be loaded into the circuit as private values using the Halo2 load_private API.

.circuit

The .circuit section holds the procedural logic of the ZK proof. In here we have statements with opcodes that are executed as understood by the VM. The statements are in the form of:

OPCODE ARG_NUM HEAP_TYPE HEAP_INDEX ... HEAP_TYPE HEAP_INDEX

where:

ElementDescription
OPCODEThe opcode we wish to execute
ARG_NUMThe number of arguments given to this opcode
(Note the VM should be checking the correctness of this as well)
HEAP_TYPEType of the heap to do lookup from (variables or literals)
(This is prepended to every HEAP_INDEX)
HEAP_INDEXThe location of the argument on the heap.
(This is supposed to be repeated ARG_NUM times)

In case an opcode has a return value, the value shall be pushed to the heap and become available for later references.

.debug

TBD

Syntax Reference

Variable Types

TypeDescription
EcPointElliptic Curve Point.
EcFixedPointElliptic Curve Point (constant).
EcFixedPointBaseElliptic Curve Point in Base Field (constant).
BaseBase Field Element.
BaseArrayBase Field Element Array.
ScalarScalar Field Element.
ScalarArrayScalar Field Element Array.
MerklePathMerkle Tree Path.
Uint32Unsigned 32 Bit Integer.
Uint64Unsigned 64 Bit Integer.

Literal Types

TypeDescription
Uint64Unsigned 64 Bit Integer.

Opcodes

OpcodeDescription
EcAddElliptic Curve Addition.
EcMulElliptic Curve Multiplication.
EcMulBaseElliptic Curve Multiplication with Base.
EcMulShortElliptic Curve Multiplication with a u64 wrapped in a Scalar.
EcGetXGet X Coordinate of Elliptic Curve Point.
EcGetYGet Y Coordinate of Elliptic Curve Point.
PoseidonHashPoseidon Hash of N Elements.
MerkleRootCompute a Merkle Root.
BaseAddBase Addition.
BaseMulBase Multiplication.
BaseSubBase Subtraction.
WitnessBaseWitness an unsigned integer into a Base.
RangeCheckPerform a (either 64bit or 253bit) range check over some Base
LessThanStrictStrictly compare if Base a is lesser than Base b
LessThanLooseLoosely compare if Base a is lesser than Base b
BoolCheckEnforce that a Base fits in a boolean value (either 0 or 1)
CondSelectSelect either a or b based on if cond is 0 or 1
ZeroCondSelectOutput a if a is zero, or b if a is not zero
ConstrainEqualBaseConstrain equality of two Base elements from the heap
ConstrainEqualPointConstrain equality of two EcPoint elements from the heap
ConstrainInstanceConstrain a Base to a Circuit's Public Input.

Built-in Opcode Wrappers

OpcodeFunctionReturn
EcAddec_add(EcPoint a, EcPoint b)(EcPoint)
EcMulec_mul(EcPoint a, EcPoint c)(EcPoint)
EcMulBaseec_mul_base(Base a, EcFixedPointBase b)(EcPoint)
EcMulShortec_mul_short(Base a, EcFixedPointShort b)(EcPoint)
EcMulVarBaseec_mul_var_base(Base a, EcNiPoint)(EcPoint)
EcGetXec_get_x(EcPoint a)(Base)
EcGetYec_get_y(EcPoint a)(Base)
PoseidonHashposeidon_hash(Base a, ..., Base n)(Base)
MerkleRootmerkle_root(Uint32 i, MerklePath p, Base a)(Base)
BaseAddbase_add(Base a, Base b)(Base)
BaseMulbase_mul(Base a, Base b)(Base)
BaseSubbase_sub(Base a, Base b)(Base)
WitnessBasewitness_base(123)(Base)
RangeCheckrange_check(64, Base a)()
LessThanStrictless_than_strict(Base a, Base b)()
LessThanLooseless_than_loose(Base a, Base b)()
BoolCheckbool_check(Base a)()
CondSelectcond_select(Base cond, Base a, Base b)(Base)
ZeroCondSelectzero_cond(Base a, Base b)(Base)
ConstrainEqualBaseconstrain_equal_base(Base a, Base b)()
ConstrainEqualPointconstrain_equal_point(EcPoint a, EcPoint b)()
ConstrainInstanceconstrain_instance(Base a)()

Decoding the bincode

An example decoder implementation can be found in zkas' decoder.rs module.