Low-Level InterfaceΒΆ

In the following we will explain the basic of the C++ interface by walking trough Utils/paper-example.cpp.

template<class T>
void run(char** argv, int prime_length);

MP-SPDZ heavily uses templating to allow to reuse code between different protocols. run() is a simple example of this. The entire virtual machine in the Processor directory is built on the same principle. The central type is a type representing a share in a particular type.

// bit length of prime
const int prime_length = 128;

// compute number of 64-bit words needed
const int n_limbs = (prime_length + 63) / 64;

Computation modulo a prime requires to fix the number of limbs (64-bit words) at compile time. This allows for optimal memory usage and computation.

if (protocol == "MASCOT")
    run<Share<gfp_<0, n_limbs>>>(argv, prime_length);
else if (protocol == "CowGear")
    run<CowGearShare<gfp_<0, n_limbs>>>(argv, prime_length);

Share types for computation module a prime (and in \(\mathrm{GF}(2^n)\)) generally take one parameter for the computation domain. gfp_ in turn takes two parameters, a counter and the number of limbs. The counter allows to use several instances with different parameters. It can be chosen freely, but the convention is to use 0 for the online phase and 1 for the offline phase where required.

else if (protocol == "SPDZ2k")
    run<Spdz2kShare<64, 64>>(argv, 0);

Share types for computation modulo a power of two simply take the exponent as parameter, and some take an additional security parameter.

Names N;
int my_number = atoi(argv[1]);
int n_parties = atoi(argv[2]);
int port_base = 9999;
Server::start_networking(N, my_number, n_parties, "localhost", port_base);

All implemented protocols require point-to-point connections between all parties. Names objects represent a setup of hostnames and IPs used to set up the actual connections. Server::start_networking() provides a way where every party connects to party 0 on a specified location (localhost in this case), which then broadcasts the locations of all parties. The base port number is used to derive the port numbers for the parties to listen on (base + party number). See the the Names class for other possibilities such as a text file containing hostname and port number for each party.

CryptoPlayer P(N);

The networking setup is used to set up the actual connections. CryptoPlayer uses encrypted connection while PlainPlayer does not. If you use several instances (for several threads for example), you must use an integer identifier as the second parameter, which must differ from any other by at least the number of parties.

// initialize fields
T::clear::init_default(prime_length);

We have to use a specific prime for computation modulo a prime. This deterministically generates one of the desired length if necessary. For computation modulo a power of two, this does not do anything.

T::clear::next::init_default(prime_length, false);

For computation modulo a prime, it is more efficient to use Montgomery representation, which is not compatible with the MASCOT offline phase however. This line initializes another field instance for MASCOT without using Montgomery representation.

// must initialize MAC key for security of some protocols
typename T::mac_key_type mac_key;
T::read_or_generate_mac_key("", P, mac_key);

Some protocols use an information-theoretic tag that is constant throughout the protocol. This codes reads it from storage if available or generates a fresh one otherwise.

// global OT setup
BaseMachine machine;
if (T::needs_ot)
    machine.ot_setups.push_back({P});

Many protocols for a dishonest majority use oblivious transfer. This block runs a few instances to seed the oblivious transfer extension. The resulting setup only works for one thread. For several threads, you need to add sufficiently many instances to ot_setups and set BaseMachine::thread_num (thread-local) to a different consecutive number in every thread.

// keeps tracks of preprocessing usage (triples etc)
DataPositions usage;
usage.set_num_players(P.num_players());

To help keeping track of the required preprocessing, it is necessary to initialize preprocessing instances with a DataPositions variable that will store the usage.

// initialize binary computation
T::bit_type::mac_key_type::init_field();
typename T::bit_type::mac_key_type binary_mac_key;
T::bit_type::part_type::read_or_generate_mac_key("", P, binary_mac_key);
GC::ShareThread<typename T::bit_type> thread(N,
        OnlineOptions::singleton, P, binary_mac_key, usage);

While this example only uses arithmetic computation, you need to initialize binary computation as well unless you use the compile-time option NO_MIXED_CIRCUITS.

// output protocol
typename T::MAC_Check output(mac_key);

Some output protocols use the MAC key to check the correctness.

// various preprocessing
typename T::LivePrep preprocessing(0, usage);
SubProcessor<T> processor(output, preprocessing, P);

In this example we use live preprocessing, but it is also possible to read preprocessing data from disk by using Sub_Data_Files instead. You can use a live preprocessing instances to generate preprocessing data independently, but many protocols require that a SubProcessor instance has been created as well. The latter essentially glues an instance of the output and the preprocessing protocol together, which is necessary for Beaver-based multiplication protocols.

// input protocol
typename T::Input input(processor, output);

Some input protocols depend on preprocessing and an output protocol, which is reflect in the standard constructor. Other constructors are available depending on the protocol.

// multiplication protocol
typename T::Protocol protocol(P);

This instantiates a multiplication protocol. P is required because some protocols start by exchanging keys for pseudo-random secret sharing.

int n = 1000;
vector<T> a(n), b(n);
T c;
typename T::clear result;

Remember that T stands for a share in the protocol. The derived type T::clear stands for the cleartext domain. Share types support linear operations such as addition, subtraction, and multiplication with a constant. Use T::constant() to convert a constant to a share type.

input.reset_all(P);
for (int i = 0; i < n; i++)
    input.add_from_all(i);
input.exchange();
for (int i = 0; i < n; i++)
{
    a[i] = input.finalize(0);
    b[i] = input.finalize(1);
}

The interface for all protocols proceeds in four stages:

  1. Initialization. This is required to initialize and reset data structures in consecutive use.
  2. Local data preparation
  3. Communication
  4. Output extraction

This blueprint allows for a minimal number of communication rounds.

protocol.init_dotprod(&processor);
for (int i = 0; i < n; i++)
    protocol.prepare_dotprod(a[i], b[i]);
protocol.next_dotprod();
protocol.exchange();
c = protocol.finalize_dotprod(n);

The initialization of the multiplication sets the preprocessing and output instances to use in Beaver multiplication. next_dotprod() separates dot products in the data preparation phase.

output.init_open(P);
output.prepare_open(c);
output.exchange(P);
result = output.finalize_open();

cout << "result: " << result << endl;
output.Check(P);

The output protocol follows the same blueprint except that it is necessary to call the checking in order to verify the outputs.

T::LivePrep::teardown();

This frees the memory used for global key material when using homomorphic encryption. Otherwise, this does not do anything.