Checkpoint the party/runtime stack before share-program and malicious-mode work.

Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

View file

@ -19,7 +19,12 @@ 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. See also the [ideal functionalities](@ref ideal_functionalities)
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}
@ -87,8 +92,8 @@ auto [k0, k1] = dpf::make_dpf(X{1000}, std::uint64_t{1});
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`, and `dpf::nyble` are outputs, not
domains. See [Input types](@ref input_types).
`_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}
@ -102,6 +107,7 @@ Many outputs can share one leaf when they fit.
| `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<T, N>` | `N` lanes, no carry between them | [vec.hpp](@ref dpf/vec.hpp) |
@ -109,7 +115,7 @@ Many outputs can share one leaf when they fit.
| `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 | [secret_share.hpp](@ref dpf/secret_share.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(
@ -118,6 +124,12 @@ auto [k0, k1] = dpf::make_dpf(
// assign the payload later; eval before assign throws
```
Shamir shares are `shamir::share<T, Party, K, N>`. 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<T, N>` is `N` lanes of an ordinary output with no carry between
lanes; the construction is on [Output types](@ref output_types).
@ -216,6 +228,10 @@ or zip two parties' buffers.
## Comparisons and ranges {#tour_dcf}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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
@ -284,6 +300,10 @@ ideal figures [F_DCF](@ref dcf.hpp), [F_BDCF](@ref blocked_dcf.hpp), [F_IC](@ref
## Verifiable and extractable keys {#tour_vdpf}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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
@ -308,6 +328,10 @@ auto [e0, e1] = dpf::make_dpf(std::uint8_t{42}, std::uint64_t{7},
## Many points at once {#tour_multipoint}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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.
@ -326,6 +350,10 @@ auto [k0, k1] = dpf::make_multipoint(alphas, betas);
## A vector with one programmable coordinate {#tour_ppvc}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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,
@ -343,6 +371,10 @@ const auto op = scheme::open(st, 0, 0x5a, std::uint8_t{40});
## Tree shapes: classic and Half-Tree {#tour_trees}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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
@ -358,6 +390,10 @@ about `4n`, and `1.5N` calls for a full-domain evaluation versus `2N`.
## Two-party keygen without a dealer: Doerner–Shelat {#tour_ds}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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.
@ -410,6 +446,10 @@ a round of its own beyond that split.
### Two-party socket walk without p2 (IKNP) {#tour_iknp}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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`),
@ -482,6 +522,10 @@ party mesh [trio.hpp](@ref dpf/net/trio.hpp).
## Multiplication and circuits: Beaver {#tour_beaver}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\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
@ -497,12 +541,75 @@ 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
<div class="eli5"><b>ELI5.</b> 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.</div>
\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
<div class="eli5"><b>ELI5.</b> 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.</div>
\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`.
@ -554,6 +661,10 @@ three `eval_it_dpf3` values is the point function. See
## Grotto: math after a public offset {#tour_grotto}
\htmlonly
<div class="eli5"><b>ELI5.</b> 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.</div>
\endhtmlonly
Open `eta = x - r`. Then cheap public corrections give rich functions of `x`
without another tree walk.
@ -567,7 +678,8 @@ without another tree walk.
| 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 |
| LUTs | Constant, easy, dyadic, range, window, principal |
| 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 |
@ -599,8 +711,15 @@ 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). The degree-0 exact
LUTs follow Appendix D of the same paper. Other LUT calls are a knot
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.
@ -618,17 +737,55 @@ Ship keys over ASIO peers with `dpf::asio::make_dpf`.
## 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): verifiability, programmability, comparisons, multipoint, three servers, dealer-free keygen, Beaver, Grotto.
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
<div class="tldr"><b>TL;DR.</b> 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 &amp; MPC and Logging &amp; statistics.</div>
\endhtmlonly