SIEVEIR Trait Methods

Here we will describe the methods of the SIEVEIR trait, which can be implemented when interfacing a new backend with the ZK-SecreC compiler. Some of the methods are required for all backends, some are optional.

Arithmetic and boolean operations

All of these methods are required. The parameter m is the modulus of the wires, represented with the type NatType. Constants are of the type Integer, which is a type synonym for BigInt from num_bigint.

Method and

fn and(&self, m: &NatType, w1: &Wire, w2: &Wire) -> Wire;

Logical conjunction.

Method xor

fn xor(&self, m: &NatType, w1: &Wire, w2: &Wire) -> Wire;

Logical exclusive or.

Method not

fn not(&self, m: &NatType, w1: &Wire) -> Wire;

Logical negation.

Method mul

fn mul(&self, m: &NatType, w1: &Wire, w2: &Wire) -> Wire;

Modular multiplication.

Method add

fn add(&self, m: &NatType, w1: &Wire, w2: &Wire) -> Wire;

Modular addition.

Method mulc

fn mulc(&self, m: &NatType, w1: &Wire, c2: &Integer) -> Wire;

Modular multiplication between value on wire and constant value.

Method addc

fn addc(&self, m: &NatType, w1: &Wire, c2: &Integer) -> Wire;

Modular addition between value on wire and constant value.

Using instance and witness streams

These are not strictly required but strongly recommended as otherwise instances and witnesses cannot be used. The methods get_instance_wr and get_witness_wr are not required if the backend does not support wire ranges.

The type Value represents any ZK-SecreC value but here it is assumed to be a scalar $pre value.

Method add_instance

fn add_instance(&self, m: &NatType, x: &Value);

Add a new value x to the instance stream of modulus m.

Method get_instance

Add a new value x to the instance stream of modulus m.

fn get_instance(&self, m: &NatType) -> Wire;

Get value of a new wire from the instance stream of modulus m. This function is called sequentially so n-th call loads the n-th value from the instance. The value must be added to the instance using add_instance before it can be read with get_instance or get_instance_wr.

Method get_instance_wr

fn get_instance_wr(&self, m: &NatType, n: usize) -> WireRange;

Get n values from instance to a new wire range. This is equivalent to calling get_instance n times but may be more efficient if the backend supports vectorized get_instance. The n values must be added to the instance stream using add_instance before they can be read with get_instance_wr.

Method add_witness

fn add_witness(&self, m: &NatType, x: &Value);

Add a new value x to the witness stream of modulus m.

Method get_witness

fn get_witness(&self, m: &NatType) -> Wire;

Get value of a new wire from the witness stream of modulus m. This function is called sequentially so n-th call loads the n-th value from the witness. The value must be added to the witness using add_witness before it can be read with get_witness or get_witness_wr.

Method get_witness_wr

fn get_witness_wr(&self, m: &NatType, n: usize) -> WireRange;

Get n values from witness to a new wire range. This is equivalent to calling get_witness n times but may be more efficient if the backend supports vectorized get_witness. The n values must be added to the witness stream using add_witness before they can be read with get_witness_wr.

Assertions and field switches

Of these, only assert_zero and assert_eq_scalar_vec are required.

Method assert_zero

fn assert_zero(&self, m: &NatType, w: &Wire);

Assert that the value on the wire is zero.

Method assert_eq

fn assert_eq(&self, m1: &NatType, w1: &Wire, m2: &NatType, w2: &Wire);

Assert that values on two wires (of different moduli) are equal.

Method assert_eq_scalar_vec

fn assert_eq_scalar_vec(&self, m1: &NatType, w1: &Wire, m2: &NatType, wires: Vec<Wire>);

Assert that the value of w1 has binary representation based on values of wires.

value(w1) = value(wires[0]) + 2*value(wires[1]) + 4*value(wires[2]) + ...

Used for more efficiently implemementing boolean circuit evaluation.

Method bool2int

fn bool2int(&self, m: &NatType, w1: &Wire, output_modulus: &NatType) -> Wire;

Convert a boolean wire of modulus m to an integer wire of modulus output_modulus.

Method int_field_switch

fn int_field_switch(&self, m: &NatType, w1: &Wire, output_modulus: &NatType) -> Wire;

Switch the field of w1 to new modulus m. For conversion of boolean to uint we use bool2int calls instead.

Constants, cloning, copying, and dropping wires

Of these methods, const_bool, zero_wire, clone_wire, and drop_wire are required.

Method const_bool

fn const_bool(&self, m: &NatType, b: bool) -> Wire;

Load a constant boolean onto a fresh wire.

Method const_uint

fn const_uint(&self, m: &NatType, x: &Value) -> Wire;

Load a constant integer onto a fresh wire. Similar to add_instance + get_instance and add_witness + get_witness but for public values.

Method zero_wire

fn zero_wire(&self, m: &NatType) -> Wire;

Construct wire that won't be used.

Method clone_wire

fn clone_wire(&self, w1: &Wire) -> Wire;

Clone a wire. Both input and output wire will be deallocated separately. If the underlying wire representation is a raw pointer the clone will have to either copy the pointed data or deal with reference counting. Input and output have the same modulus.

Method copy_bool

fn copy_bool(&self, m: &NatType, w1: &Wire) -> Wire;

Semantically identical to clone_wire where wires have modulus 2. Internally used to make sure that textual IR1 output is identical to our earlier versions.

Method drop_wire

fn drop_wire(&self, wire: &mut Wire);

Deallocate a wire. Called automatically when the wire goes out of scope. This method should free any memory that is not freed automatically.

Handling wire ranges

The methods declare_new_wire_range and declare_delete_wire_range are optional and are not called from the frontend, they are intended to be called from the backend itself. They are used only by backends that implement AllocationTrait.

The other methods are required only if the backend supports wire ranges.

Method zero_wire_range

fn zero_wire_range(&self, m: &NatType, n: usize) -> WireRange;

Construct a wire range of length n that won't be used.

Method clone_wire_range

fn clone_wire_range(&self, wr: &WireRange) -> WireRange;

Clone a range of wires.

Method index_wire_range

fn index_wire_range(&self, wr: &WireRange, i: usize) -> Wire;

Index a range of wires, returning the wire with index i. The first wire in a range has index 0.

Method slice_wire_range

fn slice_wire_range(&self, wr: &WireRange, ir: &IndexRange) -> WireRange;

Slice a range of wires. The struct IndexRange is defined in sieve.rs as follows:

pub struct IndexRange {
    pub first: usize,
    pub length: usize,
}

giving the index of the first wire and the length of the slice.

Method wire_to_wire_range

fn wire_to_wire_range(&self, w: &Wire) -> WireRange;

Convert a wire into a length-1 wire range.

Method drop_wire_range

fn drop_wire_range(&self, wr: &mut WireRange);

Drop a wire range. Called automatically when the wire range goes out of scope. This method should free any memory that is not freed automatically.

Method create_vector

fn create_vector(&self, m: &NatType, wocs: Vec<WireOrConst>) -> WireRange;

Copy wires or constant values to fresh wires in a wire range, so that they can be used in vectorized operations.

Method declare_new_wire_range

fn declare_new_wire_range(&self, alloc: &Allocation);

Allocate a range of wires.

Method declare_delete_wire_range

fn declare_delete_wire_range(&self, alloc: &Allocation);

Deallocate a range of wires.

Sieve functions and plugins

These methods are optional.

Functions defined with the sieve keyword in ZK-SecreC code are defined in the backend by first calling the method begin_function, then calling the methods corresponding to the body of the function, and then calling the method end_function. The backend should not execute the methods called between begin_function and end_function calls immediately but save them as a function definition and execute them when the defined function is later called using apply, vectorized_apply, or switch_apply. If necessary, the backend may use a different implementation of WireTrait for wires inside a function definition than for ordinary wires, as is done in the dummy backend.

Plugins (extended_arithmetic, vectors, permutation_check) can be supported through the methods plugin_apply and plugin_apply_wr. The semantics and a sample implementation of those plugins can be seen in the code of the dummy backend.

The disjunction plugin is special and uses the method switch_apply instead, also requiring support for sieve functions. The iter plugin is also special and uses vectorized_apply.

Method begin_function

fn begin_function(&self, sieve_fn: &String, output_moduli: &Vec<&NatType>, input_moduli: Vec<&NatType>) -> (Vec<Wire>, usize);

Begin a sieve function definition. Returns wires corresponding to the inputs and a UID for the sieve function (which must be different each time begin_function is called).

Method end_function

fn end_function(&self, wires_res: Vec<WireOrConst>);

End a sieve function definition. wires_res contains for each return value the wire where it was computed or a constant if this return value is constant.

Method apply

fn apply(&self, uid: usize, output_moduli: Vec<&NatType>, input_wires: Vec<&Wire>) -> Vec<Wire>;

Apply a sieve function with the given uid in a non-vectorized way to the values on the input wires.

Method vectorized_apply

fn vectorized_apply(&self, uid: usize, output_moduli: Vec<&NatType>, input_moduli: Vec<&NatType>, env_moduli: &Vec<NatType>, env_wires: Vec<&Wire>, wrs: Vec<&WireRange>) -> Vec<WireRange>;

Apply a sieve function with the given uid in a vectorized way to the values on the input wire ranges wrs. The function is first partially applied to the wires in env_wires and resulting function is then applied in a vectorized way to wrs.

Each wire range in wrs has the same length and the output wire ranges must also have the same length. The values of the ith elements of the output wire ranges are obtained by applying the sieve function to the values of the wires in env_wires and the values of the ith elements of the wire ranges in wrs.

Method switch_apply

fn switch_apply(&self, sieve_fns: &Vec<usize>, default_sieve_fn: usize, ints: &Vec<Integer>, w_modulus: &NatType, output_moduli: Vec<&NatType>, input_moduli: Vec<&NatType>, w: &Wire, wires: Vec<&Wire>, witness_count: u64) -> Vec<Wire>;

Apply one of the sieve functions with uids in sieve_fns in a non-vectorized way to the values on the input wires. The disjunction plugin is used to choose which function to apply according to which value in ints (if any) the value on wire w is equal to. If it is not equal to any, the default function (with uid default_sieve_fn) is chosen.

witness_count is the maximum number of witness values used by any of the given functions. If the chosen function uses fewer witness values then the witness stream has been padded with arbitrary values (e.g. zeroes) so that the number of values in the witness stream does not leak the chosen branch. The implementation of switch_apply must also remove those extra values from the witness stream, not just the witness values actually used by the chosen function.

Method plugin_apply

fn plugin_apply(&self, plugin: &str, operation: &str, params: Vec<&str>, modulus: &NatType, num_outputs: usize, input_wires: Vec<&Wire>) -> Vec<Wire>;

Apply a plugin operation that uses a single modulus and has single wires as inputs and outputs.

Method plugin_apply_wr

fn plugin_apply_wr(&self, plugin: &str, operation: &str, params: Vec<&str>, modulus: &NatType, output_counts: Vec<usize>, input_wrs: Vec<&WireRange>) -> Vec<WireRange>;

Apply a plugin operation that uses a single modulus and has wire ranges as inputs and outputs.

Other methods

These are optional.

Method write_headers

fn write_headers(&self, ns: &Vec<NatType>, supported_conversions: Vec<(&NatType, &NatType)>, supported_plugins: Vec<&str>);

Write IR headers if they could not be written yet when the struct implementing this trait was allocated. ns is the list of fields used in the circuit. supported_conversions is the list of conversions between fields supported in the circuit, the first element of the pair is the input field and the second element is the output field of the conversion. supported_plugins is the list of names of supported plugins.

Method get_next_wire_number

fn get_next_wire_number(&self) -> u64;

For profiling how many wires are used in each part of the code.

Method get_rel_file_size

fn get_rel_file_size(&self) -> u64;

For profiling how many lines of relation file are generated in each part of the code.

Method flush

fn flush(&self);

Flush output. Useful for text-based implementations like IR output.

Method finalize

fn finalize(&self);

Signal that code execution is finished.

Method current_status

fn current_status(&self) -> bool;

This does not seem to be used anywhere.