# A guided tour of libdpf++ {#guided_tour} This page is a map of the library in plain words. It shows short code, then points you to the full manuals when you want more. To choose which object to build, follow [which DPF](@ref which_dpf). [TOC] ## What problem does this solve? {#tour_why} Imagine a big table of zeros, with one non-zero cell at a secret place. You want two helpers to hold that table. Each helper gets a small key, not the whole table. Neither helper learns where the non-zero cell is. Together, their answers add up to the true table. That secret table is a *point function*. A *distributed point function* (DPF) is a way to share it with short keys. `libdpf++` builds those keys and evaluates them quickly in C++17. People use DPFs for private lookup (PIR), multi-party computation (MPC), and other privacy tools. This tree also ships the **network and MPC stack** those protocols need: TLS party sessions, RoundSink rounds, Beaver / Yao / arithmetic shares on leaf values, composition, leveled run logs, and paper-cost statistics. That map is [Network, parties, and MPC](@ref network_and_mpc); logging and CSVs are [Logging, statistics, and experiments](@ref experiment_costs). See also the [ideal functionalities](@ref ideal_functionalities) for what each protocol is allowed to learn. ## Prior work {#tour_prior} Elette Boyle, Niv Gilboa, and Yuval Ishai gave the point-function keys this library generates, and the later sections name Jack Doerner and abhi shelat, Xiaojie Guo, Kang Yang, Xiao Wang, Wenhao Zhang, Xiang Xie, Jiang Zhang, and Zheli Liu, and the other authors, next to the construction they described. The [bibliography](@ref bibliography) lists each paper once. ## Your first DPF {#tour_first} Include one header. Build a key for each of two parties. Evaluate at a point. Open the two shares. ```cpp #include "dpf.hpp" const std::uint8_t alpha = 42; // secret index const std::uint64_t beta = 7; // secret payload auto [k0, k1] = dpf::make_dpf(alpha, beta); auto y0 = *dpf::eval_point(k0, alpha); auto y1 = *dpf::eval_point(k1, alpha); std::uint64_t opened = dpf::reconstruct(y0, y1); // 7 ``` Leaf outputs open with subtraction: `share0 - share1`. Comparison outputs open with addition: `share0 + share1`. `dpf::reconstruct` picks the right rule from the share types. **Try next** - Full example: [eval_point.cpp](@ref evaluation/eval_point.cpp) - Eval manual: [Evaluating DPFs](@ref evaluation) - Key API: [dpf_key.hpp](@ref dpf/dpf_key.hpp) ## The feature zoo {#tour_zoo} The list on the home page is the short version. Here is the full map. ## Inputs: where the secret point lives {#tour_inputs} The *input type* is the type of the index `alpha`. Shorter inputs mean shorter keys and faster walks. | Kind | What it is for | Go deeper | | --- | --- | --- | | `uintN_t` / `intN_t` | Widths 8, 16, 32, 64 | [Input types](@ref input_types) | | 128-bit integers | Big domains | [extended types](@ref input_types) | | `modint` / `modintN_t` | Unsigned residue mod `2^N` | [modint.hpp](@ref dpf/modint.hpp) | | `xint` / `xintN_t` | XOR ring of width `N` | [xor_wrapper.hpp](@ref dpf/xor_wrapper.hpp) | | `dpf::bitstring` | Opaque bit strings | [bitstring.hpp](@ref dpf/bitstring.hpp) | | `dpf::keyword` / `keyword2` | Dictionary keys, ranked patterns | [keyword2.hpp](@ref dpf/keyword2.hpp) | | `grotto::fixedpoint` | Fixed-point domain | [fixedpoint.hpp](@ref grotto/fixedpoint.hpp) | | `wildcard_value` | Secret index assigned later | [wildcard.hpp](@ref dpf/wildcard.hpp) | | Custom types | Your own domain | [custom input rules](@ref custom_input_types) | ```cpp using X = dpf::modint<20>; // domain size 2^20 auto [k0, k1] = dpf::make_dpf(X{1000}, std::uint64_t{1}); // same index: 1000_u20 ``` Width literals sit next to those types: `100_u12` (`modint`), `7_x12` (`xint`), `dpf::literals::operator""_bitstring`, and `1.5_fixed16` through `_fixed64`. `dpf::bit`, `dpf::twobit`, `dpf::nyble`, and `dpf::gf2` through `dpf::gf264` are outputs, not domains. See [Input types](@ref input_types). ## Outputs: what sits at that point {#tour_outputs} The *output type* is the group element at `alpha`. Many outputs can share one leaf when they fit. | Kind | Group | Go deeper | | --- | --- | --- | | `uintN_t` / `intN_t` / `modint` | Additive | [Output types](@ref output_types) | | `xint` / `xor_wrapper` | Word XOR | [xor_wrapper.hpp](@ref dpf/xor_wrapper.hpp) | | `dpf::bit` | One XOR bit, packed | [bit.hpp](@ref dpf/bit.hpp) | | `dpf::twobit` | Z/4Z, packed 2-bit lanes | [twobit.hpp](@ref dpf/twobit.hpp) | | `dpf::nyble` | Z/16Z, packed nibbles | [nyble.hpp](@ref dpf/nyble.hpp) | | `gf2` … `gf264` | GF(2^k), XOR add, field multiply | [gf2.hpp](@ref dpf/gf2.hpp) | | `dpf::bitstring` | XOR string | [bitstring.hpp](@ref dpf/bitstring.hpp) | | `grotto::fixedpoint` | Fixed-point raw word | [fixedpoint.hpp](@ref grotto/fixedpoint.hpp) | | `dpf::vec` | `N` lanes, no carry between them | [vec.hpp](@ref dpf/vec.hpp) | | `dpf::wildcard_value` | Fill later | [wildcard.hpp](@ref dpf/wildcard.hpp) | | `field64` / `field128` | Prime fields | [field64.hpp](@ref dpf/field64.hpp) | | `fp61` | Field for 3-party DPFs | [fp61.hpp](@ref dpf/fp61.hpp) | | `p256` / `p256_scalar` | Curve and order | [p256.hpp](@ref dpf/p256.hpp) | | Typed shares | (2,2) additive and subtractive, (3,3) additive, (2,3) replicated, (K,N) Shamir | [secret_share.hpp](@ref dpf/secret_share.hpp), [shamir.hpp](@ref dpf/shamir.hpp) | ```cpp auto [k0, k1] = dpf::make_dpf( std::uint8_t{12}, dpf::wildcard_value{}); // assign the payload later; eval before assign throws ``` Shamir shares are `shamir::share`. Any `K` of `N` open the constant term. `(2,3)` is `shamir_share`. The fields are `fp61` and `gf2n`. For `gf2n`, `N` must be less than `2^k`. `make_dpf3` uses the `(2,3)` case on `fp61`. `examples/mwe/shamir.cpp` deals a `(3,5)` secret in `fp61` and a `(2,3)` secret in `gf28`. Packed output literals are `1_bit`, `2_twobit`, and `10_nyble`. `dpf::vec` is `N` lanes of an ordinary output with no carry between lanes; the construction is on [Output types](@ref output_types). Assigning a wildcard leaf is [leaf_nodes](@ref wildcard_assign): `compute_and_get_blinded_output_share`, `compute_and_get_leaf_share`, `reconstruct_correction_word`, or `async_assign_leaf` over a socket. The socket path is two rounds, one output-width share each way per round. The Beaver triple on the slot is preprocessing from keygen. `assign_cmp` rewrites `n` words locally and sends nothing. ## Many leaves and prefix slots {#tour_leaves} One key can carry several payloads. `dpf::at(value)` plants a value on a public prefix of length `N`. That is an *incremental* DPF: one path can read an ancestor and a leaf. ```cpp auto [k0, k1] = dpf::make_dpf( std::uint8_t{0x2a}, dpf::at<4>(std::uint8_t{5}), // high nibble std::uint8_t{9}); // full leaf, prefix 8 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}); // one payload per listed prefix; idpf(y0, y1) is prefixes 1, 2, ... auto [h0, h1] = dpf::make_dpf(std::uint8_t{0x2a}, dpf::idpf_at<4, 8>(std::uint8_t{5}, std::uint8_t{9})); ``` `dpf::out` reads slot `I` and takes the prefix from the key. `dpf::out` checks that the prefix is `W`. **Go deeper:** [incremental.hpp](@ref dpf/incremental.hpp), [placement.hpp](@ref dpf/placement.hpp), [F_IDPF](@ref incremental.hpp). ## How you evaluate {#tour_eval} | Call | What it does | | --- | --- | | `eval_point` | One input | | `eval_interval` | Inclusive range `[from, to]` | | `eval_full` | Whole domain | | `eval_sequence` | Sorted list of points | | `eval_inner_product` | Dot with weights during the walk | | `eval_until` / `idpf_eval_ctx` | Compact prefix shares up to a public bit depth | | Memoizers | Keep tree nodes between calls | | Output buffers | Hold the written shares | ```cpp auto [k0, k1] = dpf::make_dpf(std::uint8_t{42}, std::uint64_t{7}); std::vector w(11, 1); auto s0 = dpf::eval_inner_product(dpf::paired, k0, std::uint8_t{40}, std::uint8_t{50}, w); auto s1 = dpf::eval_inner_product(dpf::paired, k1, std::uint8_t{40}, std::uint8_t{50}, w); // reconstruct(s0, s1) == 7 * w[2] ``` Let `n` be the input bit length and `λ` the seed width (128 bits for the default AES PRG). A key is `Θ(n λ)` bits plus its payloads. That is the Boyle–Gilboa–Ishai CCS 2016 point key (full version [ePrint 2018/707](@ref bib_fss2018)): one correction word per level. For a small output group their Remark 3.4 stops `ν = log2(λ / log2|G|)` levels early. This generator does that: `depth` is `n` minus the log of how many outputs pack in one leaf, and those low bits select the lane. `eval_point` is `Θ(n)` expands. An inclusive interval of `L` inputs is `Θ(n + L)` expands and `L` output slots. A sorted list of `m` points is at most `O(n m)` expands and `m` slots. A full-domain eval is `Θ(2^n)`. An inner product does that same walk and keeps only the accumulator. Memoizer and buffer sizes are on the [eval manual](@ref evaluation). **Go deeper:** [Evaluating DPFs](@ref evaluation), [eval_inner_product.hpp](@ref dpf/eval_inner_product.hpp), [eval_until.hpp](@ref dpf/eval_until.hpp), examples under `examples/evaluation/`. `idpf_eval_ctx` plus `eval_until` ([ePrint 2021/017](@ref bib_poplar) EvaluateUntil) emit compact prefix shares up to a public bit depth without a full-domain walk. `idpf_agg_max` / `idpf_agg_kth` ([ePrint 2024/1190](@ref bib_idpfagg)) drive that API for order statistics — see [I-DPF max and k-th](@ref app_idpf_agg). ## Iterables {#tour_iterables} After a multi-point eval, you often want to walk only the hot leaves, or zip two parties' buffers. `eval_interval` and `eval_full` return a `subinterval_iterable`. `eval_sequence` returns a `subsequence_iterable`. `indices_set_in` walks the set bits of a bit-output iterable. `advice_bits_of` yields the low bit of each element. `batch_of` steps several bit arrays together. `tuple_as_zip` walks iterables in lockstep. `rotated_by` rotates a container. **Go deeper:** [Iterables](@ref iterables). ## Comparisons and ranges {#tour_dcf} \htmlonly
ELI5. A comparison key is not a single spike. It returns the true payload on one side of the secret point and the false payload on the other, and the shares add instead of subtract. An interval key packs the two endpoint comparisons into one key. idcf repeats a correction at every depth; cmp_prefix stops after L bits.
\endhtmlonly A *distributed comparison function* (DCF) returns a payload when a predicate holds on the secret point. The four predicates are `dpf::lt`, `dpf::leq`, `dpf::gt`, and `dpf::geq`. Each takes the true payload and an optional false payload (zero by default). `lt_at` and the same `_at` forms for `leq`, `gt`, and `geq` plant the channel on a prefix of length `N`. Evaluate with `dpf::cmp`. Shares are additive. ```cpp auto [k0, k1] = dpf::make_dpf(std::uint8_t{40}, dpf::gt(std::uint64_t{1})); // hot when the query is greater than 40 auto y0 = dpf::eval_point(dpf::cmp, k0, std::uint8_t{50}); auto y1 = dpf::eval_point(dpf::cmp, k1, std::uint8_t{50}); auto opened = dpf::reconstruct(y0, y1); // 1 ``` A wildcard comparison payload is filled later with `assign_cmp`. Eval before that throws. ```cpp auto [w0, w1] = dpf::make_dpf(std::uint8_t{40}, dpf::lt(dpf::wildcard_value{})); dpf::assign_cmp(w0, w1, std::uint64_t{7}, std::uint64_t{0}); ``` `dpf::idcf(dpf::gt(beta))` (any of the four predicates) stores a correction at every depth. `dpf::cmp` is the full point; `dpf::cmp_prefix` is the first `L` bits. `dpf::eq(if_true, if_false)` is equality, with `eq_at` on a prefix. `dpf::block_width(dpf::lt(beta))` keeps ring words only at checkpoints `B` levels apart. ```cpp auto [i0, i1] = dpf::make_dpf(std::uint8_t{40}, dpf::ic(10, 20, std::uint64_t{1})); // hot when (x - 40) mod 256 is between 10 and 20 auto in0 = dpf::eval_point(dpf::ic, i0, std::uint8_t{55}); ``` A comparison key keeps the `n`-level seed spine and one payload word per level (`Θ(n λ + n w)` bits for a `w`-bit payload). That is the DCF of Boyle, Chandran, Gilboa, Gupta, Ishai, Kumar, and Rathee, EUROCRYPT 2021 ([ePrint 2020/1392](@ref bib_dcf)). `block_width` stores a payload word every `B` levels, `Θ(n/B)` words, and the spine is unchanged. Against that EUROCRYPT 2021 comparison, which publishes a value word on every level, `block_width` is ahead on payload size: about `B` times fewer value words, with the same seed spine. Point evaluation expands the siblings between checkpoints to recover the same comparison. Point eval is one path, `Θ(n)` expands, plus an `O(n)` sum of those words. `ic` is one such key, the public-interval gate in their Figure 3, evaluated at two public shifts, still `Θ(n)`. Path paints put a unit on sibling subtrees and scale it by `if_true - if_false`. The canned ones are `lcp`, `common_prefix`, `prefix_mask`, `diverge_one_hot`, `break_bit`, and `prefix_with_length`. `path_paint(fn, if_true, if_false)` supplies the unit. Each has an `_at` form. ```cpp auto [p0, p1] = dpf::make_dpf(std::uint8_t{40}, dpf::common_prefix(std::uint64_t{1})); ``` **Go deeper:** [dcf.hpp](@ref dpf/dcf.hpp), [blocked_dcf.hpp](@ref dpf/blocked_dcf.hpp), [interval.hpp](@ref dpf/interval.hpp), ideal figures [F_DCF](@ref dcf.hpp), [F_BDCF](@ref blocked_dcf.hpp), [F_IC](@ref interval.hpp). ## Verifiable and extractable keys {#tour_vdpf} \htmlonly
ELI5. verifiable carries an extra seed on each correction word and folds it into one proof token. Equal tokens across parties mean the seeds were the honest ones. extractable is a separate weight-1 sketch in fp61: any second hot point fails it. output_mac authenticates the opened leaf, not the path.
\endhtmlonly Pass `dpf::verifiable{}` or `dpf::extractable{}` as an extra `make_dpf` argument. `verifiable` follows de Castro and Polychroniadou, EUROCRYPT 2022 ([ePrint 2021/580](@ref bib_vdpf)): one extra correction seed per level (their hash outputs `4λ` bits), a `2λ`-bit proof token, and the proof fold is part of the same `Θ(n)` walk. The stored seed is `Θ(n λ)` more bits. `extractable` adds one `fp61` sketch token to that walk. Each party folds a short proof token on a verifiable key. Equal tokens mean the walk used honest correction seeds. `extractable` is a weight-1 sketch over `fp61`. A second hot point fails the check. `dpf::updatable{}` keeps a beaver leaf so a later assign can rewrite the payload. ```cpp auto [k0, k1] = dpf::make_dpf(std::uint8_t{42}, std::uint64_t{7}, dpf::verifiable{}); auto [e0, e1] = dpf::make_dpf(std::uint8_t{42}, std::uint64_t{7}, dpf::extractable{}); ``` **Go deeper:** [verifiable.hpp](@ref dpf/verifiable.hpp), [F_VDPF](@ref verifiable.hpp), [F_Sketch](@ref verifiable.hpp). ## Many points at once {#tour_multipoint} \htmlonly
ELI5. The m secret points are placed in cuckoo buckets with three hashes, one ordinary point key per bucket, so the key grows with m and not with the domain. Evaluation probes the three buckets that could hold the query. One verifiable tag is a single proof for the whole set.
\endhtmlonly `make_multipoint(alphas, betas)` packs many points into cuckoo buckets, following de Castro and Polychroniadou, EUROCRYPT 2022, §4 (ePrint 2021/580): `κ = 3` hashes, one point key per bucket. Pass `dpf::verifiable{}` before the optional `multipoint_params` for a batched proof: one token. There is one point key per bucket, and the bucket count is linear in the number of points `m`, so the keys are `Θ(m n λ)` bits. Eval probes three buckets: `Θ(n)` expands. ```cpp std::vector alphas{1, 2, 3}; std::vector betas{4, 5, 6}; auto [k0, k1] = dpf::make_multipoint(alphas, betas); ``` **Go deeper:** [multipoint.hpp](@ref dpf/multipoint.hpp), [F_MPDPF](@ref multipoint.hpp). ## A vector with one programmable coordinate {#tour_ppvc} \htmlonly
ELI5. Commit binds both roots of each aligned 1-bit DPF pair under a Naor string, before anyone chooses the coordinate. Open reveals one side of each pair, which writes that coordinate or the sum of the vector onto a public index. verify checks the opened roots against the committed strings.
\endhtmlonly `dpf::ppvc` commits to a vector in `(Z/2^s Z)^n` before the hidden coordinate is chosen. The commitment binds both roots of `s` aligned 1-bit DPF pairs. Opening one side of each pair writes that coordinate, or the sum of the vector, and a shift moves it onto a public index. ```cpp using scheme = dpf::ppvc; const auto pp = scheme::setup(); const auto [com, st] = scheme::commit(pp); const auto op = scheme::open(st, 0, 0x5a, std::uint8_t{40}); ``` `dpf::k_ppvc` is `K` of those commitments. The full account is [Point-programmable vector commitments](@ref ppvc_manual). ## Tree shapes: classic and Half-Tree {#tour_trees} \htmlonly
ELI5. The default key is the CCS 2016 layout: one correction word per level, with the last few levels packed into the leaf when the output group is small. Half-Tree keeps that key shape and changes the expand to H(s) and H(s) XOR s, which is about half as many permutation calls. Section 5.2 of that paper is a different thing: two-party keygen in the COT/OLE hybrid, which this generator does not use.
\endhtmlonly The default interior PRG walks a Boyle–Gilboa–Ishai tree (CCS 2016, full version [ePrint 2018/707](@ref bib_fss2018)). Select `prg::aes128_ccr` as the *interior* PRG to use Half-Tree expands (`H(s)` and `H(s) XOR s`), following Guo, Yang, Wang, Zhang, Xie, Zhang, and Liu, [ePrint 2022/1431](@ref bib_halftree). Keys stay compatible with the same eval API. The number of levels is still `n`, and a key is still `Θ(n λ)` bits. Their dealer scheme keeps that length and the `n`-hash point evaluation, and states about `2n+2` random-permutation calls to generate a key versus about `4n`, and `1.5N` calls for a full-domain evaluation versus `2N`. **Go deeper:** [tree_traits.hpp](@ref dpf/tree_traits.hpp), [prg_aes_ccr.hpp](@ref dpf/prg_aes_ccr.hpp). ## Two-party keygen without a dealer: Doerner–Shelat {#tour_ds} \htmlonly
ELI5. Each party holds a share of the index and neither sends the index. Level by level they open a masked correction word. The key that comes out is the same object a dealer would have built with make_dpf. geneval uses the same opening on one public query and does not return a reusable key.
\endhtmlonly Two parties hold XOR (or additive) shares of `alpha`. A third party deals pads and learns nothing. The result matches what an honest `make_dpf` would emit. `geneval_*` does keygen and eval in one pass on a public query trie. You get shares of the answer without a reusable key. ```cpp const std::uint8_t alpha = 42; const std::uint8_t x0 = 0x13; const std::uint8_t x1 = static_cast(alpha ^ x0); const std::uint64_t beta = 7; auto [k0, k1] = dpf::make_dpf_doerner_shelat(x0, x1, beta); // index is x0 + x1; payload shares y0 + y1 reconstruct to beta const std::uint64_t y0 = 3; const std::uint64_t y1 = beta - y0; auto [a0, a1] = dpf::make_dpf_doerner_shelat( dpf::arith_input, x0, x1, beta); auto [b0, b1] = dpf::make_dpf_doerner_shelat( dpf::arith_output, x0, x1, y0, y1); ``` `geneval_point`, `geneval_interval`, `geneval_full`, and `geneval_sequence` do that keygen and the eval in one pass. They take the two shares, the public query, a `dpf::ds_randomness` tape (root sampler and pad stream), and the payload. The convenience `make_dpf_doerner_shelat` samples its pad tape locally and returns both keys with no messages. The opening follows Jack Doerner and abhi shelat, CCS 2017 ([ePrint 2017/827](@ref bib_ds)). A networked opening does one round per level, `n` rounds, because the next seeds depend on that level's correction word. Each round sends a constant number of `λ`-bit blinds and returns one `λ`-bit correction word plus advice and AND bits. Xiaojie Guo, Kang Yang, Xiao Wang, Wenhao Zhang, Xiang Xie, Jiang Zhang, and Zheli Liu ([ePrint 2022/1431](@ref bib_halftree), §5.2) generate a DPF in the COT/OLE hybrid in `n+3` rounds with no beaver-pad dealer; this opening uses that dealer tape. `geneval_*` is the same per-level opening on the public query trie. It does not return their reusable key, and with a local tape it sends nothing. Preprocessing is one correction-word pad and two AND pads per level, `Θ(n λ)` bits. `geneval_*` itself sends nothing: each live level samples those pads from the `ds_randomness` tape and folds the frontier into one correction word. Local expand time is `O(n F)` PRG expands, where `F` is the number of distinct query leaves on the trie (the call rejects more than `2^20`). That is `Θ(n)` for one point and follows the trie for an interval, a listed sequence, or a full domain. No reusable key is stored. `arith_input` first runs an `n`-step ripple carry: `n` bit-multiplication triples and `n` bit openings, then the same tree rounds. `arith_output` splits the payload and does not add a round of its own beyond that split. ### Two-party socket walk without p2 (IKNP) {#tour_iknp} \htmlonly
ELI5. IKNP turns a few slow base oblivious transfers into a long tape of correlated pads. Those pads stand in for the dealer in the Doerner–Shelat walk. The base OTs are Chou–Orlandi on P-256. When a level must stay hidden, the hash is the Boyar–Peralta AES S-box: 32 ANDs per block, which is why an oblivious tape is much longer than a reveal tape.
\endhtmlonly When there is no pad dealer, p0 and p1 sample the same Doerner–Shelat pad correlations with semi-honest OT extension after Yuval Ishai, Joe Kilian, Kobbi Nissim, and Erez Petrank, CRYPTO 2003 (`dpf::iknp::sample`), seeded by the Chou–Orlandi base OT of Tung Chou and Claudio Orlandi, LATINCRYPT 2015 ([ePrint 2015/267](@ref bib_chou)) on P-256, and install them into a local inbox. The tree walk is the same as `dist_with_*` in `party/dist_ds.hpp`. Entry points: - `dist_with_point_key_iknp` — point / half-tree / wildcard / additive input - `dist_with_extractable_point_key_iknp` - `dist_with_cmp_key_iknp` — comparison (reveal or oblivious) - `dist_with_ic_key_iknp` — interval containment A wildcard leaf is the two-party payload bind: keygen does not learn β, and `assign` writes it afterwards on the p0–p1 link. Fig-10 `updatable` rewrites are a `(2,3)` operation and stay on `dist_dpf3`. **Ideal (semi-honest).** Each party inputs its share of the point (XOR or additive) and the payload; each outputs its DPF key. With `RevealPoint`/`Reveal`, the reconstructed point (or comparison/interval prefix) may be opened because the caller asked. Without reveal, α, the peer seed, the peer payload share, and pad bits that would open the path stay hidden. Correction-word `gamma` pads are XOR shares of `(bit1·rand0)⊕(bit0·rand1)` so neither party learns the peer pad bit (learning that bit plus the opened blind would open the peer path bit). **Costs (asymptotic, then one measured case).** Let `n` be the input bit length and `λ = 128` the seed width. Write `T` for the XOR-pad tape length that `iknp_deal::add_*` demands: for a reveal point key, `T = Θ(n)` (`ncw = n` correction-word pads and `nblock = 2n + n_leaf` bit×block pads). An oblivious (non-reveal) walk also buys `n · hash_level_and_count()` bit×block pads, and `hash_level_and_count() = 8×16×10×32 = 40960` follows the Boyar–Peralta 32-AND AES S-box ([ePrint 2011/332](@ref bib_boyar)) inside `party/oblivious_hash.hpp`. | Path | Rounds | Communication | Local work | | --- | --- | --- | --- | | Dealer `make_dpf` | 0 | 0 | `Θ(n)` AES PRG expands | | DS + p2 dealer (`dist_with_*`) | `n` online (one CW open per level) | Dealer tape `Θ(n λ)` bits offline; peer opens `Θ(n λ)` bits | `Θ(n)` expands | | Half-Tree §5.2 ([ePrint 2022/1431](@ref bib_halftree)) | `n+3` in the COT/OLE hybrid | Hybrid COT/OLE (paper §5.2; not this library's opening) | CCR expands | | IKNP + DS (`dist_with_*_iknp`) | Constant-round base OT + extension, then the same `n` DS opens | Base: two Chou–Orlandi sessions of `κ = 128` OTs (33-byte P-256 points). Extension (two directions): each sends `κ ⌈T/8⌉` bytes of U plus `T · λ/8` bytes of OT correction, then `ncw · λ/8` bytes of `gamma`; B2A pads add one more extension of length `nb2a`. Peer DS messages match the dealer walk (no p2 frames). | `Θ(κ)` P-256 scalar muls for base OT; `Θ(κ T / w)` AES column expands for the extension (`w` is the chunk width); then `Θ(n)` tree expands | Compared to the p2 dealer path, IKNP removes the third party and replaces the offline `Θ(n λ)`-bit pad send with OT whose dominant term is `Θ(κ T)` bits for a reveal walk (`T = Θ(n)`). It does **not** beat Half-Tree §5.2's `n+3` round count: this library still opens one level per round after the pads exist. It also does not claim a wall-clock speedup over dealer keygen; dealer `make_dpf` stays local and free of public-key work. **Measurement** (`party_bench --case iknp_geneval_point --repeat 1 --warmup 0`, `uint8` domain so `n = 8`, reveal point, localhost sockets, one run): wall ≈ 1.08 s; p0 sent 9805 B / received 16499 B; p1 sent 11245 B / received 15059 B; harness `rounds = 47` (IKNP frames plus the `n` DS opens and the flow's eval openings); `prg_evals = 2431` each. The same harness on `iknp_wildcard_leaf` (non-reveal wildcard, so the Boyar–Peralta oblivious hash tape is live) moved tens of megabytes and took ≈ 1.8 s on one run — that is the `n · 40960` pad blow-up, not a claim about payload assign alone. `(2,3)` Shamir DPF (`party/dist_dpf3.hpp`) still needs a third key holder; there is no pure 2-party `dpf3` entry point. **Go deeper:** [doerner_shelat.hpp](@ref dpf/doerner_shelat.hpp), [geneval.hpp](@ref dpf/geneval.hpp), [F_DS](@ref doerner_shelat.hpp), [F_GenEval](@ref geneval.hpp), party mesh [trio.hpp](@ref dpf/net/trio.hpp). [iknp.hpp](@ref dpf/iknp.hpp), `party/iknp_deal.hpp`. [Bibliography](@ref bibliography). ## Multiplication and circuits: Beaver {#tour_beaver} \htmlonly
ELI5. Preprocessing gives every wire a blind. To multiply, the parties open the two inputs masked by those blinds, then fix the product with a local correction. A later gate reuses blinds it already holds and only samples new monomials. A MAC is a second share of the same width, checked in batch.
\endhtmlonly ABY2.0-style sessions open masked wires once, following Patra, Schneider, Suresh, and Yalame, USENIX Security 2021 (full version [ePrint 2020/1225](@ref bib_aby2)). A fresh triple follows Donald Beaver, [CRYPTO 1991](@ref bib_beaver), which reconstructs both masked factors. Polynomials, dots, scales, and bit-muxes share blinds. Optional MAC tags give Shark/SPDZ-style checks. One call that samples a list of formulae opens the new wires in one round. Communication is one masked value per newly opened wire, of that wire's width, plus a tag share of the same width when MACs are on. Preprocessing is one blind per wire and one product share per monomial. A later round reuses blinds it already holds and samples only the new monomials. The dealer keeps each full blind; the parties receive the additive splits. A word that has already left the key can be added and projected in one shot ([a small word](@ref arith_garble_word)). A public table on a short masked index is [FLUTE](@ref flute_lut). A secret point in a public table stays a DPF. The session above is still the interactive product. **Go deeper:** [beaver.hpp](@ref dpf/beaver.hpp), [F_Beaver](@ref beaver.hpp), [F_BeaverAuth](@ref beaver.hpp), [constrained_cmp.hpp](@ref dpf/constrained_cmp.hpp) for `F_CCMP`. ## A column of shares {#tour_shuffle} The secret position in this library is a key. One cell of a shared array is a unit DPF, a public rotate, and a dot product ([Duoram](@ref app_duoram)). A hidden shuffle is what you do when you already hold every row and you are about to open the column. Three passes, from the pairwise seeds `k01`, `k12`, and `k20`, leave one party out of each permutation. The opened order is not the stored order, and no single party can recompute it. `shuffle_hidden_pass` is one party's step. `shuffle_party` is the other helper: one permutation from `k01`, which every holder of that seed can recompute. The shuffle does not look up a cell, and it does not sort. Those stay a DPF and a DCF. **Go deeper:** [An array of shares](@ref share_shuffle), [shuffle.hpp](@ref dpf/shuffle.hpp). ## When the leaf is a circuit {#tour_yao} \htmlonly
ELI5. Eval already gave you a share of the leaf. If the next step is a bit circuit the key does not contain, split that share into XOR bits, garble the circuit, and share the answer back as a leaf.
\endhtmlonly A point leaf is subtractive, so the split is `b2y` and the return is `y2b`. A comparison leaf is additive, so the split is `a2y`. An `fss_share` opens like a point leaf. Parties 0 and 1 garbling a replicated leaf use `rss2y`; party 2 does not send. The bits are least-significant first. Both shares are arguments to the conversion. The masked `x - r` that A2B opens is uniform. The netlist is XOR, AND, XNOR, and NOT. Party 0 garbles with half-gates ([ePrint 2014/756](@ref bib_halfgates)): 32 bytes per AND, one message, XOR shares out. The block this is for is `yao::aes_mmo`, the same zero-key Matyas–Meyer–Oseas block `prg::aes128::eval` uses on a tree expand, 5120 ANDs. AES-128 under a shared key is the other packaged netlist, 6400 ANDs. The Doerner–Shelat oblivious hash still evaluates that S-box as GMW layers in [the dealer-free section](@ref tour_ds). `aes_mmo` is one of those blocks in constant rounds, for a leaf or a seed you already hold as bits. A comparison, an interval, a public-offset polynomial, and a product of two leaves do not come here. Those are a key, Grotto, or one Beaver open. `cost_pass` does not grow a Yao strategy for them. A secret if/else or a menu of blocks on those bits is stacked garbling ([a secret branch](@ref yao_stack)). The transmitted rows follow the heavier block. The leaf is still split in and shared back out. A comparison stays on the key. **Go deeper:** [A boolean function of a leaf](@ref yao_leaf), [yao.hpp](@ref dpf/yao.hpp), [yao_share.hpp](@ref dpf/yao_share.hpp), [F_Yao](@ref yao.hpp), [F_YaoShare](@ref yao_share.hpp). ## Three evaluators {#tour_dpf3} \htmlonly
ELI5. make_dpf3 gives each of three parties a pair of two-party keys, and the payload is a Shamir share in fp61, so any two evaluation shares open the value and one share is independent of it. make_it_dpf3 is not that object: each party holds an additive share of a length-256 table, and the three shares sum to the point function.
\endhtmlonly `(2,3)` point keys follow Zyskind, Yanai, and Pentland, [ePrint 2024/1658](@ref bib_dpf3), Figure 3: each evaluator key is a pair of `(2,2)`-VDPF+ keys. Each key is a Shamir share in `fp61`. Any two parties open. Keys can be verifiable, extractable, or updatable. Dealer `make_dpf3` is local. Each key is two spines, `Θ(n λ)` bits. Their evaluation section records about `2×` the key size of one two-party DPF, which is this pair of spines. Eval is two point walks, `Θ(n)`. Opening the field shares is `O(1)` `fp61` arithmetic. An updatable payload rewrite is `O(λ)`, independent of `n`. The dual-spine Doerner–Shelat form costs two copies of the two-party opening in [that section](@ref tour_ds). ```cpp auto [k1, k2, k3] = dpf::make_dpf3(std::uint8_t{42}, dpf::fp61{7}); auto y1 = dpf::eval_point(k1, std::uint8_t{42}); auto y2 = dpf::eval_point(k2, std::uint8_t{42}); auto y3 = dpf::eval_point(k3, std::uint8_t{42}); auto opened = dpf::reconstruct( dpf::as_share(k1, y1), dpf::as_share(k2, y2), dpf::as_share(k3, y3)); ``` Dual-spine Doerner–Shelat builds the same three keys from XOR shares of `alpha`. Comparison, interval, blocked, and multipoint forms are `make_dpf3_cmp`, `make_dpf3_ic`, `make_dpf3_cmp_blocked`, and `make_multipoint3`. `remake_dpf3` replaces an updatable payload. ```cpp const std::uint8_t alpha = 42; const std::uint8_t x0 = 0x13; const std::uint8_t x1 = static_cast(alpha ^ x0); auto [s1, s2, s3] = dpf::make_dpf3_doerner_shelat(x0, x1, dpf::fp61{7}); auto opened_ds = dpf::reconstruct( dpf::as_share(s1, dpf::eval_point(s1, alpha)), dpf::as_share(s2, dpf::eval_point(s2, alpha)), dpf::as_share(s3, dpf::eval_point(s3, alpha))); ``` **Go deeper:** [dpf3.hpp](@ref dpf/dpf3.hpp), [dpf3_ds.hpp](@ref dpf/dpf3_ds.hpp), [dpf3_cmp.hpp](@ref dpf/dpf3_cmp.hpp), [dpf3_multipoint.hpp](@ref dpf/dpf3_multipoint.hpp), examples `eval_dpf3_*.cpp`. `make_it_dpf3` is a different three-server object ([ePrint 2023/028](@ref bib_itdpf)): additive truth-table shares on `{0..255}`, not Shamir spines. The sum of three `eval_it_dpf3` values is the point function. See [Information-theoretic 3-server DPF](@ref it_dpf3) and [Three-server PIR](@ref app_pir3). ## Grotto: math after a public offset {#tour_grotto} \htmlonly
ELI5. The parties open the public distance eta = x − r. A polynomial, a binomial jet, a carry, or a table lookup is then a correction of shares they already have. That correction does not walk another DPF.
\endhtmlonly Open `eta = x - r`. Then cheap public corrections give rich functions of `x` without another tree walk. | Tool | Idea | | --- | --- | | Offset Horner / poly | Powers of the center, then a binomial shift | | Binomial jet | Shares of `\binom{x}{k}`; free public dots | | Ring switch | Exact move into `Z/M`, fields, or P-256 scalars | | `nmod` | Truncated Barrett reduction by a public modulus | | Representation shift | Advance a linear recurrence by public `\kappa` | | Twisted jets | `c^m \lambda^c`, including dyadic `1/2` | | Carry | Truncate, arithmetic shift, extend on shared limbs | | Prefix parity | XOR or signed prefix sums along a key | | LUT union | Several piecewise tables on one comparison and one prefix walk | | LUTs | Constant, easy, dyadic, range, window, principal, Haar and bior(5,3) | | Closed form / exact steps | Compositions and digit or bit counts | | `fixed_mul` | Fixed-point product into a chosen width | ```cpp #include "grotto.hpp" const std::uint8_t center = 12; const std::uint8_t r = 200; auto jet_keys = grotto::make_offset_jet_keys(center, 3); // after eta opens: offset_jet_shares, then offset_jet_dot auto ring = grotto::make_ring_switch_keys>(r); auto horner = grotto::make_offset_horner_keys(center); auto poly = grotto::make_offset_poly_keys(center, 4); using q16 = grotto::fixedpoint<16, std::int32_t>; auto prod = grotto::fixed_mul<16, 16>(q16{1.5}, q16{2}); ``` For degree `d` and `K` knots, offset Horner, poly, jet, and twist each store one comparison (`d ≤ 3` for Horner, `d ≤ 16` otherwise). The payload is a `dpf::vec` of the powers, so the `λ`-bit seed spine is stored once and the value-correction words grow with the vector. Key size is `Θ(n λ)` for the spine plus `Θ(d n)` bits of value words. Storrier, Vadapalli, Lyons, and Henry ([ePrint 2023/108](@ref bib_grotto)) evaluate a public piecewise polynomial from one point key by prefix parity; offset Horner is this secret-center comparison, not that spline. After `η` is public, each party runs one sequence-shaped walk on those knots, the same order as `eval_sequence`, then `O(d^2)` local arithmetic. A ring switch is one `lt` key and one point eval, `Θ(n)`. `nmod` and `fixed_mul` are local fixed-width arithmetic (`fixed_mul` uses at most 8 limbs). Carry keys are one comparison per live recipe flag, on the limb width, plus one Beaver bit-opening when the recipe multiplies share MSBs. Prefix parity on `m` endpoints is one resumed path walk, `O(m n)` expands in the worst case; that walk follows [ePrint 2023/108](@ref bib_grotto). Several piecewise LUTs share one such comparison: [make_lut_union_plan](@ref grotto/lut_union.hpp) unions their breakpoints, and [schedule_lut_union](@ref grotto/lut_union.hpp) is one `fss_cmp` of `n` rounds. The prefix walk over the union is local. The degree-0 exact LUTs follow Appendix D of the same paper. Haar and bior(5,3) tables compress a uniform grid and evaluate in \f$\Theta(1)\f$ arithmetic ([ePrint 2025/013](@ref bib_wave)). Other LUT calls are a knot search plus a constant-size Horner; exact steps loop over the word. Detail is on the Grotto pages. **Go deeper:** [jet_and_ring](@ref jet_and_ring), [repr_and_twist](@ref repr_and_twist), [grotto.hpp](@ref grotto.hpp), ideal figures on the offset headers under [Ideal functionalities](@ref ideal_functionalities). ## JSON and async I/O {#tour_io} Serialize keys with the JSON helpers. Ship keys over ASIO peers with `dpf::asio::make_dpf`. **Go deeper:** [json.hpp](@ref dpf/json.hpp), [asio.hpp](@ref dpf/asio.hpp). ## Running protocols {#tour_party} The overview of links, composition, leaf MPC, and measurement is [Network, parties, and MPC](@ref network_and_mpc). The `party/` programs run a three-role mesh (`p0`, `p1`, dealer `p2`). Flows cover Beaver, geneval, DCF, DPF3, Grotto, and adversarial checks. Use `--list` and `--tag` to filter. Composed protocols record strands on a `dpf::protocol::composer` ([compose.hpp](@ref dpf/compose.hpp)). Values are tagged with a share domain (`fss`, `a`, `b`, `rss`, `y`). An FSS leaf consumed by an ABY2.0 product gets a local `b2a` (or `fss2a`) inserted by `as`, and the leaf stays on the beaver barrier's critical path so Duoram / SUBLEQ scale cannot float before the walk. Like blinds and expansions are interned across sub-strands, and independent opens share one RoundSink round. Party-count changes use an explicit `reshare` (`rss_from_y` for y→rss). Composer-owned Beaver sessions use `beavers::schedule_objective::rounds` so sign×polynomial stays one online round; dealer benches that want Appendix-E peels keep the default `prep` objective. Express/Sabre-style audits use `fss_point_fused` / `level_walk_fused` so the sketch rides in the last CW flush. BGI early-stop is `fss_point_early_stop`; Poplar checkpoints are `level_walk_prefixes`; DCF `block_width` sizes are `level_walk_sized`. Doerner–Shelat is `level_walk_ds` / `level_walk_ds_sized` (OH AND-layers match `ds_oh_exchanges_per_level`); adaptive idpf_agg is staged `step_adaptive_prefix` → drive tail (`from_exchange_wave`) → `retain_adaptive_prefix`; keyword PIR buckets are `multipoint_fan`; multi-lane ABY is `aby_lane`; prepaid SUBLEQ expands are `defer_expand`. Party drivers use `util::drive_composed` / `util::drive_composed_trio` on `composer::default_plan()` with `u64_beaver_host` or `u64_auth_beaver_host`. A client/server query is `client_servers` (one upload, one answer). Many instances with uneven stalls are `dpf::app::run_fleet`: a worker parks instead of spinning and runs whichever side can still submit. Cross-party reshare is `reshare_with_mask`; pads are `dealer_deliver`; extra payloads share a round via `exchange_fuse`; occupied cuckoo buckets are `schedule_cuckoo_probes`. The full API and what stays outside compose are [Protocol composition](@ref protocol_compose). **Go deeper:** [trio.hpp](@ref dpf/net/trio.hpp), [compose.hpp](@ref dpf/compose.hpp), `party/registry.hpp` (in-tree). ## Suggested reading order {#tour_order} 1. [First program](@ref basics), then this tour if you want the map in prose. 2. [Capabilities](@ref capabilities) for key features; [Network, parties, and MPC](@ref network_and_mpc) for the runtime; [Logging, statistics, and experiments](@ref experiment_costs) for the run log and CSV costs. 3. [Domains](@ref input_types) and [Payloads](@ref output_types). 4. [Evaluation](@ref evaluation) and the [code examples](@ref listings). 5. [Application sketches](@ref applications). \htmlonly
TL;DR. Point keys are make_dpf: subtract to open, one correction word per level, early-stop packing when the output is small. Comparisons and intervals add instead of subtract. verifiable, extractable, and cuckoo multipoint share one paper (ePrint 2021/580). Dealer-free keygen is the Doerner–Shelat opening, with IKNP pads when nobody deals them. Three parties are either Shamir spines (any two open) or an information-theoretic table (all three add). After a public offset, Grotto corrects polynomials, carries, and tables without another walk. Live runs use the party mesh, composer, Beaver/Yao/arith on leaves, start_logging, and experiment CSVs — see Network & MPC and Logging & statistics.
\endhtmlonly