Using High-Level Functionality in C++¶
The fact that most functionality is implemented on the high level (in
the compiler) but the virtual machine running high-level code is
somewhat limited makes it desirable to call high-level functionality
from C++. MP-SPDZ supports defining functions on the high level and
calling them from C++. Functions can have integer secrets
(sint
) and (multi-)arrays thereof as
inputs and return values as well as (multi-)arrays of binary secrets
(types created using sbitvec
and
sbitintvec
).
As a simple example, consider
../Programs/Source/export-sort.py
and
../Utils/export-sort.cpp
. The Python part looks as follows:
@export
def sort(x):
print_ln('x=%s', x.reveal())
res = x.sort()
print_ln('res=%s', x.reveal())
sort(sint.Array(1000))
This makes the sorting of integer arrays of length 1000 accessible to C++. The corresponding C++ code starts similarly to the low-level code example:
#include "Machines/maximal.hpp"
int main(int argc, const char** argv)
{
assert(argc > 1);
int my_number = atoi(argv[1]);
int port_base = 9999;
Names N(my_number, 3, "localhost", port_base);
This includes all necessary headers and makes contact with the other parties on the same machine. The next step is to set up an instance of the virtual machine:
typedef Rep3Share2<64> share_type;
Machine<share_type> machine(N);
In this example, we use replicated secret sharing modulo \(2^{64}\). Next, we prepare the inputs:
int n = 1000;
ProtocolSet<share_type> set(machine.get_player(), machine);
set.input.reset(0);
for (int i = 0; i < n; i++)
{
if (my_number == 0)
set.input.add_mine(n - i);
else
set.input.add_other(0);
}
set.input.exchange();
vector<share_type> inputs;
for (int i = 0; i < n; i++)
inputs.push_back(set.input.finalize(0));
This initializes a ProtocolSet
using the virtual machine
instead of a ProtocolSetup
. This is necessary to avoid
differing MAC keys and other setup variables. Then, party 0 inputs the
numbers 1 to 1000 in reverse other, and the resulting secret shares
are stored in inputs
. Now we’re ready to call the
function:
vector<FunctionArgument> args = {{inputs, true}};
FunctionArgument res;
machine.run_function("sort", res, args);
This indicates that the function takes one argument, which is an array (as opposed to a vector, see below) and that we don’t expect a return value. Lastly, we open and check the array:
Opener<share_type> MC(machine.get_player(), machine.get_sint_mac_key());
MC.init_open();
for (auto& x : inputs)
MC.prepare_open(x);
MC.exchange();
if (my_number == 0)
{
cout << "res: ";
for (int i = 0; i < 10; i++)
cout << MC.finalize_open() << " ";
cout << endl;
}
else
{
for (int i = 0; i < n; i++)
{
auto x = MC.finalize_open();
if (x != i + 1)
{
cerr << "error at " << i << ": " << x << endl;
exit(1);
}
}
}
The Opener
class is convenience that is bound to a
communication instance (unlike MAC_Check_Base
instances,
which require the communication instance in several function calls).
You can run the example as follows:
./compile.py -E ring export-sort
make export-sort.x
for i in 0 1 2; do ./export-sort.x $i & true; done
This makes sure that all the optimizations of the protocol are used.
Vector arguments and return values¶
Instead of arrays, it is also possible to use
sint
vectors as demonstrated in
../Programs/Source/export-trunc.py
:
@export
def trunc_pr(x):
print_ln('x=%s', x.reveal())
res = x.round(32, 2)
print_ln('res=%s', res.reveal())
return res
trunc_pr(sint(0, size=1000))
The calling C++ code in ../Utils/export-trunc.cpp
looks as
follows:
int n = 1000;
vector<share_type> inputs;
for (int i = 0; i < n; i++)
inputs.push_back(share_type::constant(i, my_number));
vector<FunctionArgument> args = {inputs};
vector<share_type> results(n);
FunctionArgument res(results);
machine.run_function("trunc_pr", res, args);
This creates integer shares using public constants instead of the
input protocol as above. The FunctionArgument
instance
for both input and output are created using the vector of secret
shares without the extra true
argument.
Binary values¶
It is possible to input and output binary secrets with an
array. Consider ../Programs/Source/export-b2a.py
, which
converts arithmetic to binary shares:
@export
def b2a(res, x):
print_ln('x=%s', x.reveal())
res[:] = sint(x[:])
print_ln('res=%s', x.reveal())
b2a(sint.Array(size=10), sbitvec.get_type(16).Array(10))
This demonstrates the requirement of using an array of an
sbitvec
type with a defined number of bits (16 in this
case). sbitintvec
is a sub-class and also permissible.
The C++ calling code looks as follows:
int n = 10;
vector<share_type> outputs(n);
vector<vector<share_type::bit_type>> inputs(n);
auto& inputter = set.binary.input;
inputter.reset(0);
for (int i = 0; i < n; i++)
if (my_number == 0)
inputter.add_mine(i + 1, 16);
else
inputter.add_other(0);
inputter.exchange();
for (int i = 0; i < n; i++)
inputs.at(i).push_back(inputter.finalize(0, 16));
vector<FunctionArgument> args = {{outputs, true}, {16, inputs}};
FunctionArgument res;
machine.run_function("b2a", res, args);
This inputs the values 1 to 10 as 16-bit numbers. Note the nested
vectors for the inputs. This is due to the fact
share_type::bit_type
can only hold up to 64 bits, so for longer
bit lengths several entries have to be used.
Lastly, ../Programs/Source/export-a2b.py
covers the other
direction:
@export
def a2b(x, res):
print_ln('x=%s', x.reveal())
res[:] = sbitvec(x, length=16)
print_ln('res=%s', x.reveal())
a2b(sint(size=10), sbitvec.get_type(16).Array(10))
The calling C++ code in ../Utils/export-a2b.cpp
has to
initialize the binary shares even when they are only used for output:
int n = 10;
vector<share_type> inputs;
for (int i = 0; i < n; i++)
inputs.push_back(
share_type::constant(i + 1, my_number,
machine.get_sint_mac_key()));
vector<vector<bit_share_type>> outputs(n,
vector<bit_share_type>(1,
bit_share_type::constant(0, my_number,
machine.get_bit_mac_key())));
vector<FunctionArgument> args = {{inputs}, {16, outputs}};
FunctionArgument res;
machine.run_function("a2b", res, args);
C++ compilation¶
The easiest way is to include Machines/maximal.hpp
as in the first
example and put the C++ in code Utils/<name>.cpp
and calling
make <name>.x
in the main directory. If using oblivious transfer
or homomorphic encryption, add the following line to Makefile
:
<name>.x: $(FHEOFFLINE) $(OT)
Most of the examples work slightly differently, however, in order to
distribute the compilation load. Most notably,
../Utils/export-a2b.cpp
, which supports several protocols,
only includes Machines/minimal.hpp
and “outsources” the virtual
machine for the various protocols to Machines/export-*.cpp
, which
are all compiled separately.
Reference¶
-
class FunctionArgument¶
Inputs and outputs for functions exported in high-level code.