ZK Rust Language (ZKRust)
ZK-Rust is the main tool for writing Multi Party Computations for Partisia Platform. ZK-Rust resembles Rust in syntax, with a restricted feature set, and an alternative standard library tailored for Partisia Platform.
Further reading:
Concepts
Zk-Rust's most significant difference from Rust, is it's support for representing Secret-shared values, the basic building blocks for writing Zero-knowledge computations. This secret-sharing support extends from new syntax to types to runtime behaviour.
Secret-shared integers
The secret-sharing supported in Zk-Rust is an alternative method of
representing integers. This representation is used when working with
the Sbi
types (Sbi1
, Sbi8
, Sbi16
, etc.,) which stands for "Secret-shared
Binary Integer". These types are fully fledged integer types,
supporting a variety of infix operations (+
, -
, ^
, |
, etc.) All of
these operations are performed through secret-sharing, ensuring that the result
is itself secret-shared:
use pbc_zk::Sbi32;
pub fn my_computation() -> Sbi32 {
let public_1: i32 = 93;
let secret_1: Sbi32 = Sbi32::from(public_1);
let secret_2: Sbi32 = Sbi32::from(231);
let secret_3: Sbi32 = secret_1 + secret_2;
secret_3 * Sbi32::from(2)
}
As seen above, secrecy is contagious: All operations involving a secret-shared type produces a new secret-shared type.
This comes from the more general rule that it must be impossible to learn any secret information at the public level, a kind of rule known as Information Flow Control (IFC). The secret-sharing IFC rule is enforced by The Partisia Blockchain, which prevents leakage of sensitive information.
The following program will not compile, for example:
use pbc_zk::Sbi32;
pub fn my_computation() -> i32 {
let secret_1: Sbi32 = Sbi32::from(93);
let secret_2: Sbi32 = Sbi32::from(231);
let declassify: i32 = secret_1 + secret_2; // !! cannot assign secret to public variable!
declassify * 2
}
New SbiN
values can be created from public integers using SbiN::from(iN)
.
Secret Branching
As we've seen above, the operations available on Sbi
is constrained, due to
IFS. Another situation where Secret-shared values are less flexible than their
public counterparts is when branching.
Consider for example:
use pbc_zk::Sbi32;
pub fn my_computation() -> i32 {
let secret: Sbi32 = ...;
let mut public: i32 = 9;
if secret == Sbi32::from(4) {
public = 5; // !! Cannot assign secret to public variable!
}
public
}
The above program is disallowed by ZkRust, because it is trying to assign to
a public value while braching upon a secret. This is clearly a break of IFC,
as the value of public
after the branch would be based upon the value of
secret
, which again is a break of IFC.
It is possible to assign secrets in secret-braches:
use pbc_zk::Sbi32;
pub fn my_computation() -> Sbi32 {
let secret: Sbi32 = ...;
let mut secret_2: Sbi32 = Sbi32::from(9);
if secret == Sbi32::from(4) {
secret_2 = Sbi32::from(5);
}
secret_2
}
Here the assignment is allowed, as there is no declassification. Note though that this branch is compiled differently from public branches, and might result in some performance overhead.
Secret variables
Each contract possess a list of secret variables. These variables possess:
- A unique id.
- Secret data, secret-shared between contract's computation nodes. Loadable in
the computation by
load_sbi::<S: SecretBinary>(id: i32): S
. - Public information called metadata. Loadable in the computation by
load_metadata::<T: Public>(id: i32): T
. - An owner (blockchain account), who uploaded the variable, or in the case of computation outputs, the contract itself. This information is not available for zk-computation.
Contract users can add new variables (called inputs at this point) by invoking
the
contract's #[zk_on_secret_input]
.
Variables can additionally be created
as the result of running a computation. The public part of the ZK-contract is
responsible for managing the variables, including opening (revealing the
output of a computation) and deleting them.
Variable ids can be iterated by
calling pbc_zk::secret_variable_ids(): Iter<i32>
:
use pbc_zk::{Sbi32, secret_variable_ids, load_sbi};
pub fn sum_all_variables() -> Sbi32 {
let mut sum = Sbi32::from(0);
for variable_id in secret_variable_ids() {
sum = sum + load_sbi::<Sbi32>(variable_id);
}
sum
}
Type Structures
Rust's struct
keyword is supported for creating structure types. These
structs can either contain purely public types, or purely secret-shared types.
use pbc_zk::{Sbi16, SecretBinary};
// public
struct SomeData {
uid: i64,
info: i32,
}
#[derive(SecretBinary)]
struct Point {
x: Sbi16,
y: Sbi16,
}
#[derive(SecretBinary)]
struct Triangle {
x: Point,
y: Point,
z: Point,
}
Types with #[derive(SecretBinary)]
implements
the SecretBinary
trait,
allowing them to be loaded using
the load_sbi
function. Note that
variables
does not possess explicit types, and can be loaded as any type of equal or
fewer bits. For example, let's say variable with id 42
has 64 bits of data.
This variable can be loaded using any of the following, producing different
values for each:
load_sbi::<Sbi32>(42)
, due to32 <= 64
.load_sbi::<Sbi64>(42)
, due to64 <= 64
.load_sbi::<Point>(42)
, due to16 + 16 <= 64
.
Attempting to load variable 42
as Triangle
would result in a runtime
exception, as it would attempt to load (16 + 16)*3 = 96
bits, which variable
42
cannot provide.
Libraries
As shown in the previous examples, it is possible to import functions and data
structures from the pbc_zk
module. See Zero-knowledge Language
Reference for what is available.