Publish a scannable manual and a bibliography that links each paper back to the pages that use it.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cf8054a0b3
commit
695f8e84f7
45 changed files with 4848 additions and 338 deletions
|
|
@ -1,5 +1,18 @@
|
|||
<!-- # Evaluating DPFs {#evaluation} -->
|
||||
|
||||
On this page:
|
||||
[wildcard assign](@ref wildcard_assign),
|
||||
[memoizers](@ref memoizers),
|
||||
[buffers](@ref output_buffers),
|
||||
[point](@ref eval_point),
|
||||
[interval](@ref eval_interval),
|
||||
[full domain](@ref eval_full),
|
||||
[deferred input](@ref defer_eval),
|
||||
[inner product](@ref eval_inner_product),
|
||||
[sequence](@ref eval_sequence),
|
||||
[three-party](@ref dpf3),
|
||||
[IT 3-server](@ref it_dpf3).
|
||||
|
||||
`make_dpf(x, y)` returns one key per party. Evaluation of a key yields that
|
||||
party's share. Leaf outputs are subtractive shares: open them with
|
||||
`dpf::reconstruct`, which computes `share0 - share1`. Comparison outputs are
|
||||
|
|
@ -10,6 +23,122 @@ additive shares: `reconstruct` computes `share0 + share1`. A single-output
|
|||
`[from, to]` is inclusive. The points passed to `eval_sequence` are a
|
||||
nondecreasing range; an unsorted range throws `std::runtime_error`.
|
||||
|
||||
Let `n` be the input bit length and `λ` the PRG seed width (128 bits for
|
||||
`prg::aes128`). A `make_dpf` key stores one `λ`-bit root and, on each of
|
||||
the `n` levels, one `λ`-bit correction word and an advice bit, plus one
|
||||
leaf per output slot. Key size is `Θ(n λ)` bits plus those payloads.
|
||||
That layout is Boyle, Gilboa, and Ishai, CCS 2016 (full version
|
||||
[ePrint 2018/707](@ref bib_fss2018)), whose stated length is `n(λ+2)+λ+⌈log2|G|⌉` bits, not
|
||||
their [EUROCRYPT 2015](@ref bib_fss2015) key of `4n(λ+1)` bits. For a small output group `G`, Remark 3.4 of the full version stops
|
||||
`ν = log2(λ / log2|G|)` levels early and shortens the key by `ν(λ+2)`
|
||||
bits. This generator does that: `depth` is `n - lg(outputs_per_leaf)`,
|
||||
where `outputs_per_leaf` is how many copies of `G` fit in one `λ`-bit
|
||||
block. Those low input bits select the lane. A comparison still writes
|
||||
a value word on each remaining level. The default expand is that CCS 2016
|
||||
tree. Guo, Yang, Wang, Zhang, Xie, Zhang, and Liu ([ePrint 2022/1431](@ref bib_halftree)) keep
|
||||
the same key length and the same `n`-hash point evaluation, and state
|
||||
about `2n+2` random-permutation calls for key generation versus about
|
||||
`4n`, and `1.5N` calls for a full-domain evaluation versus `2N`.
|
||||
`prg::aes128_ccr` selects that Half-Tree expand.
|
||||
Keygen expands once per level. `at<N>` and `idpf` add leaves; the spine
|
||||
is still `n` levels. A comparison adds one payload word per level.
|
||||
`block_width<B>` stores one word every `B` levels instead, and the seed
|
||||
spine stays `Θ(n λ)`. That is ahead of Elette Boyle, Nishanth Chandran,
|
||||
Niv Gilboa, Divya Gupta, Yuval Ishai, Nishant Kumar, and Mayank Rathee
|
||||
(EUROCRYPT 2021, [ePrint 2020/1392](@ref bib_dcf)), who publish a value word on every
|
||||
level: about `B` times fewer payload words, paid for by expanding the
|
||||
siblings between checkpoints at evaluation. See the
|
||||
[bibliography](@ref bibliography).
|
||||
|
||||
`eval_point` expands those `n` levels. A basic path memoizer keeps the
|
||||
`n` nodes (`Θ(n λ)` bits) and the next call expands only the suffix after
|
||||
the common prefix. The nonmemoizing memoizer keeps one node. `reconstruct`
|
||||
of two shares, or of two or three Shamir shares, is a constant amount of
|
||||
group arithmetic.
|
||||
|
||||
`eval_point<I>` and `eval_point(dpf::out<I>, key, x)` read output slot `I`.
|
||||
`dpf::out<I, W>` is that slot with prefix length `W` checked against the
|
||||
key. A bare leaf's prefix is the input bit length. `dpf::at<N>` uses `N`.
|
||||
`dpf::prefix_deduce` (the default of `out<I>`) reads the prefix from the key.
|
||||
`eval_point(dpf::cmp, key, x)` reads the comparison channel.
|
||||
`eval_point(dpf::cmp_prefix<L>, key, x)` reads the first `L` bits of an
|
||||
`idcf` key. The [eval_point](@ref eval_point) section has the calls.
|
||||
|
||||
On an `idpf` / multilevel key, `eval_prefixes(out<I,N>, key)` walks from
|
||||
the root and materializes all `2^N` prefix shares (Poplar-style).
|
||||
`idpf_eval_ctx` / `eval_until` ([ePrint 2021/017](@ref bib_poplar)'s EvaluateUntil) resume
|
||||
under a live prefix list so the saved node count stays
|
||||
`O(|prefixes|)`, not `O(2^N)`. The I-DPF max / k-th walk in
|
||||
[dpf/idpf_agg.hpp](@ref dpf/idpf_agg.hpp) ([ePrint 2024/1190](@ref bib_idpfagg)) is that
|
||||
caller. See [eval_until](@ref dpf/eval_until.hpp) and the application
|
||||
mockup [I-DPF max and k-th](@ref app_idpf_agg).
|
||||
|
||||
## Assigning a wildcard leaf {#wildcard_assign}
|
||||
|
||||
An output wildcard is a placeholder for a payload filled after keygen.
|
||||
The type and the `dpf::wildcards` names are on
|
||||
[Output types](@ref output_types). Evaluation of an unassigned slot throws
|
||||
`std::runtime_error`.
|
||||
|
||||
In one process, each party holds a share of the payload and the two keys.
|
||||
Slot `I` is `std::get<I>(key.leaf_nodes)` (`party_key` is the key). The
|
||||
exchange is three calls:
|
||||
|
||||
\code{cpp}
|
||||
auto & w0 = std::get<0>(k0.leaf_nodes);
|
||||
auto & w1 = std::get<0>(k1.leaf_nodes);
|
||||
auto b0 = w0.compute_and_get_blinded_output_share(share0);
|
||||
auto b1 = w1.compute_and_get_blinded_output_share(share1);
|
||||
auto l0 = w0.compute_and_get_leaf_share(b1);
|
||||
auto l1 = w1.compute_and_get_leaf_share(b0);
|
||||
w0.reconstruct_correction_word(l1);
|
||||
w1.reconstruct_correction_word(l0);
|
||||
\endcode
|
||||
|
||||
`share0` and `share1` are the two parties' shares of the payload.
|
||||
`compute_and_get_blinded_output_share` also accepts a `secret_share`.
|
||||
A wrapper that is already ready takes `begin_update()` before the same
|
||||
three calls; that assign adds a difference onto the payload already
|
||||
installed.
|
||||
|
||||
Over a socket, `key.async_assign_leaf(peer, share, token)` (and
|
||||
`async_assign_leaf<I>`) runs that exchange. The method is present when
|
||||
the library is built with ASIO. That is two rounds: blinded output
|
||||
shares, then leaf shares. Each message is one share of the output (the
|
||||
second round sends the packed leaf). The Beaver triple on the wildcard
|
||||
slot is the preprocessing; keygen stored it. Local time is linear in
|
||||
the leaf width.
|
||||
|
||||
`assign_cmp` on a pair of keys rewrites the `n` value-correction words
|
||||
in place and sends nothing. `assign_cmp_local` is that patch on one key
|
||||
once `δ` and the addend share are already known. An input wildcard is
|
||||
one round: the parties exchange one `n`-bit offset share and reconstruct
|
||||
locally. See [Input types](@ref input_types).
|
||||
|
||||
A wildcard *comparison* payload is different. Generate with
|
||||
`dpf::lt(dpf::wildcard_value<Beta>{})` (or `leq` / `gt` / `geq`), then
|
||||
`dpf::assign_cmp(k0, k1, if_true, if_false)` on the pair. Eval before
|
||||
that throws. `dpf::assign_cmp_local(key, delta, addend_share)` is the
|
||||
one-key form: both parties pass the same public `delta` and additive
|
||||
shares of the absorb target.
|
||||
|
||||
\code{cpp}
|
||||
auto [k0, k1] = dpf::make_dpf(std::uint8_t{40},
|
||||
dpf::lt(dpf::wildcard_value<std::uint64_t>{}));
|
||||
dpf::assign_cmp(k0, k1, std::uint64_t{7});
|
||||
auto y0 = dpf::eval_point(dpf::cmp, k0, std::uint8_t{10});
|
||||
\endcode
|
||||
|
||||
An input wildcard masks the index. The calls are
|
||||
`offset_x.compute_and_get_share` and `offset_x.reconstruct` on
|
||||
[Input types](@ref input_types). Eager `eval_interval` / `eval_full` require
|
||||
that offset to be ready. To expand **before** assign (full-domain identity,
|
||||
then rotate once `δ` is known), use
|
||||
[Deferred input evaluation](@ref defer_eval).
|
||||
|
||||
**Defined in**\n
|
||||
@ref dpf/leaf_wrapper.hpp, @ref dpf/dpf_key.hpp, @ref dpf/incremental.hpp
|
||||
|
||||
Memoizers hold interior nodes between calls. Output buffers hold the shares
|
||||
a multi-point evaluation writes. Pass both as mutable named objects when a
|
||||
later call should reuse them. The factories
|
||||
|
|
@ -18,7 +147,7 @@ later call should reuse them. The factories
|
|||
either party's type accepts both parties. Name that type with
|
||||
`dpf::unwrap_party_key_t<std::decay_t<decltype(key)>>`.
|
||||
|
||||
# Memoizers {#memoizers}
|
||||
## Memoizers {#memoizers}
|
||||
|
||||
## Path memoizers {#path_memoizers}
|
||||
|
||||
|
|
@ -31,7 +160,8 @@ keeps one node and starts from the root on every call. A one-off
|
|||
Pass the memoizer as a mutable lvalue. The default argument is a new
|
||||
temporary, so it has no previous point to resume from. Keep a separate
|
||||
memoizer for each key you are in the middle of evaluating. A different root
|
||||
restarts the path.
|
||||
restarts the path. One point evaluation is `Θ(n)` expands. The basic
|
||||
memoizer's extra memory is the `n` saved nodes.
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
|
@ -57,6 +187,15 @@ interval rebuilds into the same allocation.
|
|||
Passing only the memoizer still allocates a fresh output buffer and returns
|
||||
`std::pair(buffer, iterable)`.
|
||||
|
||||
Let `L` be the number of inputs in the inclusive interval. The node count
|
||||
at a level is the width of that interval shifted up to the level, and the
|
||||
walk expands each of those nodes once, `Θ(n + L)` expands in total. The
|
||||
output buffer is `L` slots per selected output. A basic memoizer keeps two
|
||||
levels, `O(L)` nodes. A full-tree memoizer keeps every level, still
|
||||
`Θ(n + L)` nodes. `eval_full` is the same walk with `L = 2^n`: time and
|
||||
the output buffer are `Θ(2^n)` slots, and the basic full memoizer holds
|
||||
two levels of that tree.
|
||||
|
||||
## Sequence memoizers {#sequence_memoizers}
|
||||
|
||||
`make_sequence_recipe<Key>(begin, end)` compiles a sorted point list into a
|
||||
|
|
@ -75,7 +214,16 @@ level and reverses direction as it descends.
|
|||
`make_full_tree_sequence_memoizer<Key>(recipe)` retains every level. A key
|
||||
whose depth differs from the recipe throws `std::logic_error`.
|
||||
|
||||
# Output buffers {#output_buffers}
|
||||
Let `m` be the number of listed points. `make_sequence_recipe` makes one
|
||||
pass per level and binary-searches each live block, so the build is
|
||||
`O(n m log m)` comparisons in the worst case. The recipe stores one step
|
||||
per block per level, `O(n m)` records when every level splits. Evaluating
|
||||
it expands one node per step, at most `O(n m)` expands, and writes `m`
|
||||
shares. The double-space memoizer keeps two levels of that traversal
|
||||
(`O(m)` nodes at the widest level). The inplace memoizer keeps one level.
|
||||
The full-tree memoizer keeps every level.
|
||||
|
||||
## Output buffers {#output_buffers}
|
||||
|
||||
`output_buffer<T>` is move-only storage with `size`, iterators, `data`, and
|
||||
`operator[]`. Build it with the factory that matches the evaluation:
|
||||
|
|
@ -94,6 +242,9 @@ the evaluation overwrites every slot it is responsible for.
|
|||
reference, so the argument is a named object. The returned iterable refers
|
||||
into that buffer. Read it while the buffer is alive, and only over the
|
||||
points the iterable covers. The next evaluation overwrites those slots.
|
||||
The buffer holds one slot per input in the interval, or one slot per
|
||||
listed point. Its space is that many slots times the slot width. Packed
|
||||
`bit`, `twobit`, and `nyble` slots share words.
|
||||
|
||||
For one output, the convenience overload returns the buffer itself as the
|
||||
first element of the pair. For several output indices it returns a tuple of
|
||||
|
|
@ -111,16 +262,35 @@ rule.
|
|||
|
||||
</div>
|
||||
|
||||
# dpf::eval_point {#eval_point}
|
||||
## dpf::eval_point {#eval_point}
|
||||
|
||||
`eval_point(key, x)` evaluates output 0 at one input.
|
||||
`eval_point<I>(key, x)` selects another output. Two or more indices,
|
||||
`eval_point<0, 1>(key, x)`, return a tuple of shares rather than handles.
|
||||
`eval_point(key, x, path)` continues a path memoizer.
|
||||
|
||||
`eval_point(dpf::out<I>, key, x, path)` and `eval_point(dpf::cmp, key, x, path)`
|
||||
are the same walk with an explicit channel. Comparison results are additive
|
||||
shares.
|
||||
`eval_point(dpf::out<I>, key, x, path)` and
|
||||
`eval_point(dpf::out<I, W>, key, x, path)` are the same walk with an
|
||||
explicit slot. `W` must equal that slot's prefix.
|
||||
`eval_point(dpf::cmp, key, x, path)` reads the comparison channel.
|
||||
`eval_point(dpf::cmp_prefix<L>, key, x, path)` reads an `idcf` prefix of
|
||||
`L` bits. Comparison results are additive shares. An unassigned wildcard
|
||||
comparison throws until `assign_cmp`; see
|
||||
[Assigning a wildcard leaf](@ref wildcard_assign).
|
||||
|
||||
\code{cpp}
|
||||
auto [k0, k1] = dpf::make_dpf(
|
||||
std::uint8_t{0x2a},
|
||||
dpf::at<4>(std::uint8_t{5}),
|
||||
std::uint8_t{9});
|
||||
auto hi = *dpf::eval_point(dpf::out<0, 4>, k0, std::uint8_t{0x2a});
|
||||
auto leaf = *dpf::eval_point(dpf::out<1, 8>, k0, std::uint8_t{0x2a});
|
||||
|
||||
auto [c0, c1] = dpf::make_dpf(std::uint8_t{40},
|
||||
dpf::idcf(dpf::gt(std::uint64_t{1})));
|
||||
auto full = dpf::eval_point(dpf::cmp, c0, std::uint8_t{50});
|
||||
auto pref = dpf::eval_point(dpf::cmp_prefix<4>, c0, std::uint8_t{50});
|
||||
\endcode
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
|
@ -129,7 +299,7 @@ shares.
|
|||
|
||||
</div>
|
||||
|
||||
# dpf::eval_interval {#eval_interval}
|
||||
## dpf::eval_interval {#eval_interval}
|
||||
|
||||
`eval_interval(key, from, to)` evaluates every input from `from` through
|
||||
`to`. The iterable yields one share per input, in that order. Optional
|
||||
|
|
@ -144,7 +314,7 @@ selected output.
|
|||
|
||||
</div>
|
||||
|
||||
# dpf::eval_full {#eval_full}
|
||||
## dpf::eval_full {#eval_full}
|
||||
|
||||
`eval_full(key)` is the closed interval from
|
||||
`std::numeric_limits<Input>::min()` through `max()`. The buffer and
|
||||
|
|
@ -159,7 +329,120 @@ size both for that domain.
|
|||
|
||||
</div>
|
||||
|
||||
# dpf::eval_sequence {#eval_sequence}
|
||||
## Deferred input evaluation {#defer_eval}
|
||||
|
||||
Additive input blinding evaluates in tree coordinates `x ↦ x + δ`, where
|
||||
`δ` is reconstructed by `assign_wildcard_input` into `offset_x`. Eager
|
||||
`eval_interval` folds that map into the traversed range and throws if the
|
||||
offset is not ready.
|
||||
|
||||
When `δ` is still unknown, call `defer_eval_interval` or `defer_eval_full`:
|
||||
|
||||
1. Expand the **full** input domain at identity into a full-sized buffer
|
||||
(`make_output_buffer_for_full`).
|
||||
2. After assign, `.get()` on the returned `deferred_rotated_subinterval`
|
||||
rotates by `offset_x(0)` and yields the logical `[from, to]` (including
|
||||
wrap-around when `from > to` in domain-walk order).
|
||||
|
||||
Interior-only prep (assigned input, leaf still a wildcard) is
|
||||
`defer_traverse_interval`. Full-domain interior with both still unset is
|
||||
`defer_traverse_full`.
|
||||
|
||||
Keep the deferred object alive for the lifetime of any `.get()` range.
|
||||
Buffers must outlive both.
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
||||
- <b class="tab-title">defer_eval.cpp</b> \include{cpp} evaluation/defer_eval.cpp
|
||||
|
||||
</div>
|
||||
|
||||
**Defined in**\n
|
||||
@ref dpf/deferred_rotated_subinterval.hpp, @ref dpf/eval_interval.hpp,
|
||||
@ref dpf/eval_full.hpp
|
||||
|
||||
## dpf::eval_inner_product {#eval_inner_product}
|
||||
|
||||
`eval_inner_product` multiply-accumulates DPF shares against another vector
|
||||
during the walk. It does not write the output vector.
|
||||
|
||||
Three local forms:
|
||||
|
||||
- **Batched leaf walk** (no tag): `eval_inner_product(key, from, to, weights, memo)`.
|
||||
One output, weights in `eval_interval` layout, same batched exterior AES as
|
||||
that walk, O(1) accumulator.
|
||||
- **`dpf::paired`** (row-wise): one weight row per input. A scalar pairs with
|
||||
one output; a `tuple` / `array` zips several outputs (leaf slots or an
|
||||
ancestor prefix plus the leaf) off one path. Products are summed.
|
||||
- **`dpf::columns`** (transposed): one output, several weight streams, one
|
||||
accumulator per stream. Products stay apart. A stream is `w[i]` or `w(i)`.
|
||||
`dpf::project` maps the share first; `dpf::also` sees it unmapped.
|
||||
|
||||
A single-stream `columns` result matches `paired` on that stream. A
|
||||
single-output batched leaf walk matches `paired` when the interval is
|
||||
leaf-aligned so covering-leaf weights equal the clipped domain points.
|
||||
Unaligned intervals still weight every lane of the covering leaves (same
|
||||
layout as one-key `eval_interval` buffers). Cohort evaluation uses a separate
|
||||
*interleaved* leaf layout (`cohort_index`); `interleave_leaves` builds that
|
||||
order from per-key buffers (see `dpf/interleave_leaves.hpp` and
|
||||
`dpf/cohort.hpp`).
|
||||
|
||||
Pass `dpf::paired`. One element of the other vector is consumed per input, in
|
||||
the same order as `eval_interval` or `eval_sequence`.
|
||||
|
||||
A scalar element pairs with one output:
|
||||
|
||||
```cpp
|
||||
eval_inner_product(dpf::paired, key, from, to, weights);
|
||||
eval_full_inner_product(dpf::paired, key, weights);
|
||||
```
|
||||
|
||||
A `std::tuple` or `std::array` element pairs componentwise with the output
|
||||
indices in the template pack. Those outputs may be several slots on the same
|
||||
leaf, or an ancestor prefix slot and the leaf. Both are read from one path:
|
||||
|
||||
```cpp
|
||||
eval_inner_product<0, 1>(dpf::paired, key, from, to, rows);
|
||||
eval_sequence_inner_product<0, 1>(key, begin, end, rows);
|
||||
eval_sequence_inner_product<0, 1>(key, recipe, begin, end, rows);
|
||||
```
|
||||
|
||||
`share * component` must be defined. The product type must support `+`.
|
||||
The walk costs the same as the interval or sequence it follows. Each
|
||||
input adds a constant amount of arithmetic per paired output or per
|
||||
column. The accumulator is the only result; there is no output vector
|
||||
of shares.
|
||||
The point list for a sequence or recipe is sorted nondecreasing, same as
|
||||
`eval_sequence`. The recipe overload also checks that the list length matches
|
||||
the recipe.
|
||||
|
||||
`dpf::columns` is the same walk with the products kept apart. One output,
|
||||
several weight streams, one accumulator per stream:
|
||||
|
||||
```cpp
|
||||
auto [sum, dot, sq] = eval_sequence_inner_product<0>(
|
||||
dpf::columns, key, begin, end, std::tie(ones, r, r2));
|
||||
```
|
||||
|
||||
A stream is anything with `w[i]` or `w(i)`, so a challenge can be a function
|
||||
of the list index instead of a stored vector. `i` is the position in the
|
||||
point list, or in the interval in `eval_interval` order. `dpf::project(fn)`
|
||||
maps the share before the multiply. `dpf::also(fn)` is called as
|
||||
`fn(i, x, share)` on the share before that map, which is how a mailbox slot
|
||||
is updated in the same walk. Pass either tag, both, or neither.
|
||||
|
||||
The older single-output range walk (no `dpf::paired`) is still the batched
|
||||
leaf inner product: `eval_inner_product<I>(key, from, to, weights, memo)`.
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
||||
- <b class="tab-title">eval_inner_product.cpp</b> \include{cpp} evaluation/eval_inner_product.cpp
|
||||
|
||||
</div>
|
||||
|
||||
## dpf::eval_sequence {#eval_sequence}
|
||||
|
||||
`eval_sequence(key, begin, end, tag)` evaluates a sorted list.
|
||||
`dpf::return_output_only_tag_` stores one share per listed point.
|
||||
|
|
@ -170,6 +453,13 @@ The iterable still yields one share per listed point, in list order.
|
|||
`memo` is a sequence memoizer bound to `recipe`. Omit `memo` to allocate a
|
||||
`double_space` workspace for that call.
|
||||
|
||||
`eval_sequence_breadth_first(key, begin, end, buffer)` writes one share
|
||||
per listed point, expanding a level at a time. The list is still sorted.
|
||||
`eval_sequence_breadth_first(dpf::out<I>, key, begin, end)` allocates the
|
||||
buffer and returns it. The `out<I, W>` form checks the prefix the same way
|
||||
`eval_point` does. The cost is the same order as recipe `eval_sequence`
|
||||
on that list: at most `O(n m)` expands and `m` output slots.
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
||||
|
|
@ -177,7 +467,7 @@ The iterable still yields one share per listed point, in list order.
|
|||
|
||||
</div>
|
||||
|
||||
# Buffered PRG {#buffered_prg}
|
||||
## Buffered PRG {#buffered_prg}
|
||||
|
||||
`dpf::randomness::buffered_prg<PRG, Ts...>` (alias
|
||||
`dpf::randomness::aes_buffered_prg<Ts...>`) is a forward cursor with one
|
||||
|
|
@ -190,9 +480,100 @@ it is. `sampled<I>()` is how far `get` and `fill` have advanced.
|
|||
roles. `value_at(role, index)` and `mask_at(role, index)` are independent
|
||||
streams, and a repeated index returns the same element.
|
||||
|
||||
`get` and `fill` of `q` elements do `Θ(q)` PRG work. The cursor keeps
|
||||
`per_stream_buffer_elems` elements per stream. `at` reads one absolute
|
||||
index and does not move the cursor. A `lane_table` keeps one cache
|
||||
window per role (the constructor's `window`, default 256) for the value
|
||||
stream and one for the mask stream. A repeated index returns the same
|
||||
element. `fill_values` / `fill_masks` of `q` elements are `Θ(q)`.
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
||||
- <b class="tab-title">buffered_prg.cpp</b> \include{cpp} evaluation/buffered_prg.cpp
|
||||
|
||||
</div>
|
||||
|
||||
## Three-party (2,3) DPF {#dpf3}
|
||||
|
||||
`make_dpf3(α, β)` builds three evaluator keys after Guy Zyskind, Avishay Yanai, and Alex "Sandy" Pentland, [ePrint 2024/1658](@ref bib_dpf3), Figure 3:
|
||||
two VDPF+ spines plus Shamir embedding in `fp61`. `eval_point` returns a
|
||||
field share; open with `dpf::reconstruct` on any two (or all three)
|
||||
`dpf::as_share` values. Tags: `verifiable`, `extractable`, `updatable`
|
||||
(Fig. 10 in-place payload update).
|
||||
|
||||
`make_dpf3_doerner_shelat(x0, x1, β)` is the dual-spine Doerner–Shelat path:
|
||||
XOR shares of `α`, same clear `β`. Socket orchestration lives in
|
||||
`party/dist_dpf3.hpp` (`dist_with_dpf3_key`): after keygen, role **p0** holds
|
||||
party 1, **p2** holds party 2, **p1** holds party 3. Path bits open to p0/p1;
|
||||
p2 does not learn `α`. Dist keys are verifiable; payload updates use dealer
|
||||
`updatable` keys or `remake_dpf3`.
|
||||
|
||||
Comparison / interval: `make_dpf3_cmp`, `make_dpf3_cmp_blocked`, `make_dpf3_ic`.
|
||||
Multipoint: `make_multipoint3` (cuckoo buckets of point keys).
|
||||
|
||||
`make_dpf3` is local dealer work. Each evaluator key is two VDPF+ spines,
|
||||
`Θ(n λ)` bits with a constant factor over one two-party key. `eval_point`
|
||||
is two walks, `Θ(n)` expands, then a field scale. Opening two or three
|
||||
`as_share` values is a constant number of `fp61` operations. An
|
||||
`updatable` rewrite is four leaf patches and an offset refresh, `O(λ)`
|
||||
and independent of `n`. `make_dpf3_doerner_shelat` is two
|
||||
Doerner–Shelat spines: twice the rounds and the pad tape of one
|
||||
two-party opening (see [Doerner–Shelat](@ref tour_ds)). Comparison,
|
||||
interval, and multipoint forms add the same extra material as the
|
||||
two-party comparison, interval, or cuckoo packing, on top of those spines.
|
||||
|
||||
## Information-theoretic 3-server DPF {#it_dpf3}
|
||||
|
||||
`make_it_dpf3(α, β)` ([ePrint 2023/028](@ref bib_itdpf)) is a different object from
|
||||
`make_dpf3`. Each of three parties holds an additive share of the
|
||||
characteristic vector on `{0..255}`; the **sum** of all three
|
||||
`eval_it_dpf3` values is the point function. Any single key is
|
||||
independent of `(α, β)`. For this domain size the key is a full
|
||||
truth-table share (`N = 256` words), not the paper's matching-vector
|
||||
packing. PIR is `eval_it_dpf3_inner_product` on each server; the three
|
||||
dots sum to the record. See [it_dpf3.hpp](@ref dpf/it_dpf3.hpp) and
|
||||
[Three-server PIR](@ref app_pir3).
|
||||
|
||||
Socket (2+1) keygen lives in `party/dist_ds.hpp`. The two-party peer that
|
||||
replaces the pad dealer is IKNP (`party/iknp_deal.hpp`,
|
||||
`dist_with_*_iknp`): same Doerner–Shelat walk after Jack Doerner and abhi
|
||||
shelat, CCS 2017 ([ePrint 2017/827](@ref bib_ds)), with pads from `dpf::iknp::sample`
|
||||
(Ishai, Kilian, Nissim, and Petrank, CRYPTO 2003; Chou–Orlandi base OT,
|
||||
[ePrint 2015/267](@ref bib_chou)). There is no two-party `(2,3)` Shamir path —
|
||||
`dist_dpf3` needs three key holders. Fig-10 `updatable` payload rewrites
|
||||
also stay on `dist_dpf3`.
|
||||
|
||||
**IKNP vs dealer vs Half-Tree (costs).** With input bit length `n` and
|
||||
seed width `λ = 128`, a reveal point tape has length `T = Θ(n)`
|
||||
(`ncw = n`, `nblock = 2n + n_leaf`). An oblivious (non-reveal) tape adds
|
||||
`n · 40960` bit×block pads because each level runs the Boyar–Peralta
|
||||
32-AND S-box hash ([ePrint 2011/332](@ref bib_boyar); see `hash_level_and_count()`).
|
||||
|
||||
- Dealer `make_dpf`: 0 rounds, 0 bytes, `Θ(n)` AES expands.
|
||||
- DS + p2 (`dist_with_*`): dealer sends `Θ(n λ)` bits of pads offline;
|
||||
online is `n` rounds and `Θ(n λ)` bits of peer opens (guided tour
|
||||
[Doerner–Shelat](@ref tour_ds)).
|
||||
- Half-Tree §5.2 ([ePrint 2022/1431](@ref bib_halftree)): `n+3` rounds in the COT/OLE hybrid
|
||||
with no beaver-pad dealer. This library's IKNP path does **not** use
|
||||
that hybrid; it keeps the per-level DS open after OT-sampled pads.
|
||||
- IKNP + DS: two Chou–Orlandi sessions of `κ = 128` base OTs, then two
|
||||
OT-extension directions whose U-matrix and correction traffic is
|
||||
`Θ(κ T)` bits, then the same `n` DS opens as the dealer walk (no p2
|
||||
frames). Local work adds `Θ(κ)` P-256 scalar muls and the AES column
|
||||
expands of the extension.
|
||||
|
||||
One localhost measurement (`party_bench --case iknp_geneval_point
|
||||
--repeat 1 --warmup 0`, `uint8` so `n = 8`, reveal): ≈ 1.08 s wall;
|
||||
about 10–16 KiB per direction on the p0–p1 link; harness `rounds = 47`
|
||||
and `prg_evals = 2431`. That is a measurement of this harness, not a
|
||||
claimed speedup over dealer keygen or over Half-Tree §5.2.
|
||||
|
||||
**Code samples**\n
|
||||
<div class="tabbed">
|
||||
|
||||
- <b class="tab-title">eval_dpf3_point.cpp</b> \include{cpp} evaluation/eval_dpf3_point.cpp
|
||||
- <b class="tab-title">eval_dpf3_doerner_shelat.cpp</b> \include{cpp} evaluation/eval_dpf3_doerner_shelat.cpp
|
||||
- <b class="tab-title">eval_dpf3_cmp_ic.cpp</b> \include{cpp} evaluation/eval_dpf3_cmp_ic.cpp
|
||||
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue