Browse Source

Start on comparison DPFs

Ian Goldberg 1 year ago
parent
commit
f7ae6edadb
5 changed files with 139 additions and 7 deletions
  1. 3 1
      Makefile
  2. 28 0
      cdpf.cpp
  3. 101 0
      cdpf.hpp
  4. 7 0
      dpf.hpp
  5. 0 6
      rdpf.hpp

+ 3 - 1
Makefile

@@ -5,7 +5,8 @@ LDFLAGS=-ggdb
 LDLIBS=-lbsd -lboost_system -lboost_context -lboost_chrono -lboost_thread -lpthread
 
 BIN=prac
-SRCS=prac.cpp mpcio.cpp preproc.cpp online.cpp mpcops.cpp rdpf.cpp
+SRCS=prac.cpp mpcio.cpp preproc.cpp online.cpp mpcops.cpp rdpf.cpp \
+    cdpf.cpp
 OBJS=$(SRCS:.cpp=.o)
 ASMS=$(SRCS:.cpp=.s)
 
@@ -38,3 +39,4 @@ online.o: duoram.tcc
 mpcops.o: mpcops.hpp types.hpp mpcio.hpp coroutine.hpp bitutils.hpp
 rdpf.o: rdpf.hpp mpcio.hpp types.hpp coroutine.hpp bitutils.hpp dpf.hpp
 rdpf.o: prg.hpp aes.hpp rdpf.tcc mpcops.hpp
+cdpf.o: cdpf.hpp types.hpp dpf.hpp prg.hpp bitutils.hpp aes.hpp

+ 28 - 0
cdpf.cpp

@@ -0,0 +1,28 @@
+#include <bsd/stdlib.h> // arc4random_buf
+#include "bitutils.hpp"
+#include "cdpf.hpp"
+
+// Generate a pair of CDPFs with the given target value
+std::tuple<CDPF,CDPF> CDPF::generate(value_t target)
+{
+    CDPF dpf0, dpf1;
+
+    // Pick two random seeds
+    arc4random_buf(&dpf0.seed, sizeof(dpf0.seed));
+    arc4random_buf(&dpf1.seed, sizeof(dpf1.seed));
+    // Ensure the flag bits (the lsb of each node) are different
+    dpf0.seed = set_lsb(dpf0.seed, 0);
+    dpf1.seed = set_lsb(dpf1.seed, 1);
+    dpf0.whichhalf = 0;
+    dpf1.whichhalf = 1;
+
+    return std::make_tuple(dpf0, dpf1);
+}
+
+// Generate a pair of CDPFs with a random target value
+std::tuple<CDPF,CDPF> CDPF::generate()
+{
+    value_t target;
+    arc4random_buf(&target, sizeof(target));
+    return generate(target);
+}

+ 101 - 0
cdpf.hpp

@@ -0,0 +1,101 @@
+#ifndef __CDPF_HPP__
+#define __CDPF_HPP__
+
+#include <tuple>
+#include "types.hpp"
+#include "dpf.hpp"
+
+// DPFs for doing comparisons of (typically) 64-bit values. We use the
+// technique from:
+//
+// Kyle Storrier, Adithya Vadapalli, Allan Lyons, Ryan Henry.
+// Grotto: Screaming fast (2 + 1)-PC for Z_{2^n} via (2, 2)-DPFs
+//
+// The idea is that we have a pair of DPFs with 64-bit inputs and a
+// single-bit output.  The outputs of these DPFs are the same for all
+// 64-bit inputs x except for one special one (target), where they're
+// different, but if you have just one of the DPFs, you can't tell what
+// the value of target is.  The construction of the DPF is a binary
+// tree, where each interior node has a 128-bit value, the low bit of
+// which is the "flag" bit.  The invariant is that if a node is on the
+// path leading to the target, then not only are the two 128-bit values
+// on the node (one from each DPF) different, but their flag (low) bits
+// are themselves different, and if a node is not on the path leading to
+// the target, then its 128-bit value is the _same_ in the two DPFs.
+// Each DPF also comes with an additive share (target0 or target1) of
+// the random target value.
+//
+// Given additive shares x0 and x1 of x, two parties can determine
+// bitwise shares of whether x>0 as follows: exchange (target0-x0) and
+// (target1-x1); both sides add them to produce S = (target-x).
+// Notionally consider (but do not actually construct) a bit vector V of
+// length 2^64 with 1s at positions S+1, S+2, ..., S+(2^63-1), wrapping
+// around if the indices exceed 2^64-1.  Now consider (but again do not
+// actually do) the dot product of V with the full evaluation of the
+// DPFs.  The full evaluations of the DPFs are random bit vectors that
+// differ in only the bit at position target, so the two dot products
+// (which are each a single bit) will be a bitwise shraring of the value
+// of V at position target.  Note that if V[target] = 1, then target =
+// S+k for some 1 <= k <= 2^63-1, then since target = S+x, we have that
+// x = k is in that same range; i.e. x>0 as a 64-bit signed integer (and
+// similarly if V[target] = 0, then x <= 0.
+//
+// So far, this is all standard, and for DPFs of smaller depth, this is
+// the same technique we're doing for RDPFs.  But we can't do it for
+// vectors of size 2^64; that's too big.  Even for 2^32 it would be
+// annoying.  The observation made in the Grotto paper is that you can
+// actually compute this bit sharing in time linear in the *depth* of
+// the DPF (that is, logarithmic in the length of V), for some kinds of
+// vectors V, including the "single block of 1s" one described above.
+//
+// The key insight is that if you look at any _interior_ node of the
+// tree, the corresponding nodes on the two DPFs will be a bit sharing
+// of the sum of all the leaves in the subtree rooted at that interior
+// node: 0 if target is not in that subtree, and 1 if it is.  So you
+// just have to find the minimal set of interior nodes such that the
+// leaves of the subtrees rooted at those nodes is exactly the block of
+// 1s in V, and then each party adds up the flag bits of those leaves.
+// The result is a bit sharing of 1 if V[target]=1 and 0 if V[target]=0;
+// that is, it is a bit sharing of V[target], and so (as above) of the
+// result of the comparison [x>0].  You can also find and evaluate the
+// flag bits of this minimal set in time and memory linear in the depth
+// of the DPF.
+//
+// So at the end, we've computed a bit sharing of [x>0] with local
+// computation linear in the depth of the DPF (concretely, fewer than
+// 200 AES operations), and only a *single word* of communication in
+// each direction (exchanging the target{i}-x{i} values).  Of course,
+// this assumes you have one pair of these DPFs lying around, and you
+// have to use a fresh pair with a fresh random target value for each
+// comparison, since revealing target-x for two different x's but the
+// same target leaks the difference of the x's. But in the 3-party
+// setting (or even the 2+1-party setting), you can just have the server
+// precompute a bunch of these pairs in advance, and hand bunches of the
+// first item in each pair to player 0 and the second item in each pair
+// to player 1, at preprocessing time (a single message from the server
+// to each of player 0 and player 1), and these DPFs are very fast to
+// compute, and very small (< 1KB each) to transmit and store.
+
+// See also dpf.hpp for the differences between these DPFs and the ones
+// we use for oblivious random access to memory.
+
+struct CDPF : public DPF {
+    // Additive and XOR shares of the target value
+    RegAS as_target;
+    RegXS xs_target;
+    // The extra correction word we'll need for the right child at the
+    // final leaf layer; this is needed because we're making the tree 7
+    // layers shorter than you would naively expect (depth 57 instead of
+    // 64), and having the 128-bit labels on the leaf nodes directly
+    // represent the 128 bits that would have come out of the subtree of
+    // a (notional) depth-64 tree rooted at that depth-57 node.
+    DPFnode leaf_cw;
+
+    // Generate a pair of CDPFs with the given target value
+    static std::tuple<CDPF,CDPF> generate(value_t target);
+
+    // Generate a pair of CDPFs with a random target value
+    static std::tuple<CDPF,CDPF> generate();
+};
+
+#endif

+ 7 - 0
dpf.hpp

@@ -1,6 +1,7 @@
 #ifndef __DPF_HPP__
 #define __DPF_HPP__
 
+#include <vector>
 #include "prg.hpp"
 
 // We have two major kinds of distributed point functions (DPFs): ones
@@ -47,6 +48,12 @@ struct DPF {
     // correction flag bits: the one for level i is bit i of this word
     value_t cfbits;
 
+    // The depth
+    inline nbits_t depth() const { return cw.size(); }
+
+    // The seed
+    inline node get_seed() const { return seed; }
+
     // Descend from a node at depth parentdepth to one of its children
     // whichchild = 0: left child
     // whichchild = 1: right child

+ 0 - 6
rdpf.hpp

@@ -50,12 +50,6 @@ struct RDPF : public DPF {
     // depth
     static size_t size(nbits_t depth);
 
-    // The depth
-    inline nbits_t depth() const { return cw.size(); }
-
-    // The seed
-    inline node get_seed() const { return seed; }
-
     // Do we have a precomputed expansion?
     inline bool has_expansion() const { return expansion.size() > 0; }