libdpf/doc/pages/compose.md
Ryan Henry 0d22946a0e 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>
2026-09-28 05:59:19 -06:00

204 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Protocol composition {#protocol_compose}
\htmlonly
<div class="eli5"><b>ELI5.</b> Record every expand, multiply, and open as a node with a share domain. Identical work is interned. Independent opens share one RoundSink round; a dependency chain becomes successive waves. The schedule is what a hand-tuned Express or Duoram party would have written by hand.</div>
\endhtmlonly
`dpf::protocol::composer` ([compose.hpp](@ref dpf/compose.hpp)) records
multi-protocol strands on one sink. Values are tagged with a share
domain matching [secret_share.hpp](@ref dpf/secret_share.hpp):
| Domain | Meaning |
| --- | --- |
| `fss` | FSS / DPF leaf share |
| `a` | (2,2) additive |
| `b` | (2,2) subtractive |
| `rss` | (2,3) replicated |
| `y` | (3,3) additive (RSS product factor) |
Party-count changes are never implied: use `reshare` (or `rss_from_y`
for `y`→`rss`). Local casts use `as`.
## Building a schedule
```cpp
dpf::protocol::composer c(/*party=*/0);
auto seed = c.input(dpf::protocol::domain::fss, 16);
auto leaf = c.fss_point(seed, /*depth=*/8, /*slot_bytes=*/16);
auto p = c.default_plan(); // == schedule(); RoundSink uses this
// p.rounds() == p.exchange_waves() == 8 (no empty compute-only sink rounds)
```
Drive with [drive](@ref dpf::protocol::drive) or party
`util::drive_composed` / `util::drive_composed_trio`, optionally with
`drive_options` (`from_exchange_wave`, `compact_sink`, `beavers`).
Express/Sabre audits use `util::schedule_fused_audit` (or `fss_point_fused`).
The same plan lowers onto the RoundSink batch loop with
`plan_to_schedule` → [`schedule_session`](@ref dpf::protocol::schedule_session)
→ `finish_schedule`, or the one-shot `drive_via_schedule`. Each exchange wave
becomes a [`schedule_round`](@ref dpf::protocol::schedule_round) whose
`produce` runs on this thread when that instance's peer slot is ready (the
`schedule_session::drive` scan). Rounds carry an
[`edge_id`](@ref dpf/net/edge_mesh.hpp) on an [`edge_mesh`](@ref dpf/net/edge_mesh.hpp)
(star / clique / dealer), a [`receive_rule`](@ref dpf::protocol::receive_rule)
(`domain_open`, `copy_peer`, `field_sum`, `any_two`, `verify_*`, `eq_check`),
optional `branch` (skip send) and `next` (jump after an open).
[`session_host`](@ref dpf/session_host.hpp) queues micro-plans on a durable mesh.
Pad graphs live in [pad_graphs.hpp](@ref dpf/pad_graphs.hpp)
(`du_atallah_mul_graph`, `star_upload_answer_graph`); IKNP setup in
[iknp_graphs.hpp](@ref dpf/iknp_graphs.hpp); PIRsona / hushmap builders in
[mesh_apps.hpp](@ref dpf/mesh_apps.hpp). Named application plans and
`drive_star` / `submit_and_drive` live in [app_plans.hpp](@ref dpf/app_plans.hpp).
Replayable seeds and paper cost CSVs: see [Experiments](@ref experiment_costs).
In short, `dpf::experiment` / `app::measure_plan` / `app::run_measured` attach a
per-thread master seed (default random, or `replay`) and emit CSV breakdowns.
`plan::rounds()` equals `exchange_waves()` and `slot_bytes_all().size()` —
the sink and the scheduler share one count. Trailing compute-only DAG waves
still appear in `waves()` / `wave(i)` but do not allocate RoundSink rounds.
Composer-owned ABY sessions default to
`beavers::schedule_objective::rounds` so online sign×polynomial stays
one round. Dealer benches that want Appendix-E peels keep `prep`.
Independent circuits use `aby_lane(i)`. Live δ openings use
`drive_options::beavers` (`util::u64_beaver_host` wraps
`party_batch_stepper`).
## Hand-schedule shapes the API covers
| Shape | Call | What it saves |
| --- | --- | --- |
| L‖R PRG stretch | `expand_pair` / default `level_walk` | Half the AES vs separate child expands |
| Shared-seed fan (Grotto LUT) | `fan` of `fss_cmp` on one seed | One expand per level, not N×depth |
| Several piecewise LUTs | `schedule_lut_union` | One `fss_cmp`; the union's prefix walk is local |
| Express / Sabre audit | `fss_point_fused` / `schedule_fused_audit` | Sketch in last CW — no +1 exchange wave |
| BGI Remark 3.4 early-stop | `fss_point_early_stop` | Drop ν interactive CW rounds |
| Poplar / idpf prefix checkpoints | `level_walk_prefixes` | Prefix share after each CW, no extra rounds |
| Adaptive idpf_agg | `step` → drive tail → `retain` → `step`… | One packed L‖R open per depth; child bit never on the wire |
| DCF `block_width` | `level_walk_sized` | Per-level CW slot bytes |
| Doerner–Shelat keygen | `level_walk_ds` / `level_walk_ds_sized` | blind‖CW‖advice‖AND + OH AND-layers |
| Keyword PIR / PSI buckets | `multipoint_fan` / `exchange_pack` | Bucket CWs pack; answers one open |
| RSS mul + neighbor refresh | `rss_product_replicated` / `rss_from_y` | One y-exchange, not a reconstructing open |
| FSS leaf → ABY scale | `aby_product` after `fss_point` | Leaf stays on the beaver barrier critical path |
| Multi-lane ABY | `aby_lane(i)` / `aby_product(..., lane)` | Independent barriers, shared wave |
| Round-aware Beaver | `composer::aby<Ring>()` | `schedule_objective::rounds` |
| Prepaid expand / rotate | `defer_expand` / `rotate_share` | Zero online FSS rounds (SUBLEQ offline) |
| Duoram leaf_later | `leaf_later_walk` / `apply_leaf_correction` | Path CWs only; leaf apply is local |
| RSS column, hidden order | `shuffle_hidden` | Three `shuffle_send` waves. `aux` is the left-out party: 2, then 0, then 1 |
Adaptive prefixes: `step_adaptive_prefix` schedules only the next packed
open; drive with `from_exchange_wave = exchanges_flushed` and a sink sized
by `plan::slot_bytes_from` (`compact_sink = true`); then
`retain_adaptive_prefix` adds one local tip. Never unroll a full-depth
adaptive walk into one static schedule.
3PC RSS: `rss_from_y` schedules a `domain::y` exchange (neighbor receive).
`util::drive_composed_trio` splits peer maps per exchange — `y` on the
neighbor ring, `dealer_deliver` from p2, everything else on p0↔p1.
Cross-party `reshare` refuses a reconstructing open; use
`reshare_with_mask`. Opens use domain algebra (`a`/`fss` sum, `b`
subtractive, `y` copy, optional `field_open::fp61`). Authenticated Beaver
sessions size barriers as `auth_opening` and drive through
`u64_auth_beaver_host`. `exchange_fuse` packs extra payloads into one open.
`schedule_cuckoo_probes` turns occupied bucket ids into `multipoint_fan`.
## Client and servers {#compose_client}
Keyword PIR and both three-server PIRs are a star, not a correction-word
walk. `client_servers(servers, query_bytes, answer_bytes)` records two
waves and nothing between the servers:
1. Upload. The slot is `servers * query_bytes` (every key in one round).
2. Answer. The slot is `servers * answer_bytes`. It depends on the upload,
so it cannot share that wave.
`drive` copies the peer's message into the exchange node. It does not add
the shares. A two-party sink stands in for one client and one server; the
slot width is still the full parallel payload, which is what
`dpf::app::exercise` reports as `bytes`.
```cpp
auto q = c.client_servers(/*servers=*/2, /*query_bytes=*/256, /*answer_bytes=*/4);
auto p = c.default_plan();
// p.rounds() == 2
// p.slot_bytes(0) == 512, p.slot_bytes(1) == 8
```
## Parking and the fleet {#compose_fleet}
`drive` used to spin until the peer flushed, then throw. A step that
simply takes a long time looks like that failure, and a pool that always
resumes the side already waiting on a receive never runs the peer who
could unblock it.
`drive_options::park_if_waiting` returns instead. `drive_cursor` remembers
the wave, the exchange index, and whether the submit already happened.
The next `drive` with that cursor receives if the peer has caught up, or
parks again. `one_exchange` stops after a single completed round so a
scheduler can interleave other instances.
`dpf::app::run_fleet(composer, instances, chaos_seed)` is that scheduler.
Each instance is a pair of parties on an in-process sink. Workers prefer
a side that still has a submit to do over a side parked on a receive, and
among those they prefer the one further behind. Delays are per side and
per step. They do not line up, which is the case that makes round-robin
and "always resume the waiter" stall.
`dpf::app::run(name, composer, expect_rounds)` is the one-shot experiment
used by `examples/applications/`. It drives both parties and prints
```
name rounds=R bytes=B
```
`bytes` is the sum of one lane's exchange slots.
Opcodes from `beaver_delta` (300) through `beaver_delta + 1023` are δ
barriers. `user_base` (1000) sits inside that window, so a hand-rolled
kernel opcode in that range is skipped as a beaver. Use 5000 and up for
kernels `drive` must run or reject.
## What eval_full reuses
`eval_full(key, buffer)` keeps a thread-local workspace for that key type.
The same key evaluated again does not rebuild the interior. A different
key still does. Leaves of one block are stretched eight at a time
(`eval_x8`). Pass your own memoizer when two evaluations on one thread
must not share that cache.
DS OH: `level_walk_ds(..., oh=true)` emits
`net::ds_oh_exchanges_per_level` (80) AND-layer opens per level. Size sinks
with `net::compose_ds_slot_bytes` (schedule-exact) or the conservative
`net::ds_walk_slot_bytes` for hand `dist_ds` paths.
\include{cpp} protocol/compose_schedule.cpp
## Gap analysis {#compose_gaps}
| Item | Status |
| --- | --- |
| Point-key stretch | Builtin `fss_expand_pair` runs `prg::aes128`; `fss_step+1` XORs the opened correction onto the children |
| Setup plus walk | `iknp_setup_graph` / `plan_with_pad_setup` — pad frames are `schedule_round`s |
| Edge mesh | `edge_mesh` + `make_memory_star` / `make_memory_clique`; `drive_via_schedule(plan, mesh, …)` |
| Receive rules | `field_sum`, `any_two`, `verify_sketch` / `verify_proof`, `eq_check` in `apply_peer_slot` |
| Jump / host | `schedule_round::next` + `session_host` for SUBLEQ / idpf_agg / hushmap ops |
| PIRsona / hushmap | `pirsona_bitmore_fetch`, `pirsona_gd_update_graph`, `hushmap_add_schedule` |
| 2PC → RSS | `reshare` casts to a `y` share (p2 holds 0) and `rss_from_y`. `reshare_fresh` adds a dealer-sampled zero mask |
| Auth openings | `auth_beaver_host<Ring>` / `u64_auth_beaver_host`. `party_auth_batch_stepper::apply_peer` rejects a bad tag |
| One DS sink | `ds_walk_slot_bytes` is the envelope of the hand walk and `compose_ds_slot_bytes` (5 peer opens per level + OH + mux) |
| Wide opens | Every multiple of 8 bytes is a lane add/sub (`field_open::fp61` for the Mersenne field) |
| Fuse offset | `exchange_fuse` stores the segment offset in `aux` (`uint32_t`) |
Still outside this scheduler: the cuckoo PRP, OPRF evaluation, the full IKNP wire body inside each pad `produce` (frames and count are in the schedule; `iknp::sample` still owns the crypto), and key-specific control-bit / verifiable / Doerner–Shelat advice logic. Those last ones override the builtin kernels. Trio multi-edge `drive_via_schedule` still needs `edge_sinks` wired from the party helper.
### Closed earlier
Incremental drive, domain-correct `a`/`b`/`y` opens, split y/2PC peer maps, OH AND-layers, `level_walk_ds_sized`, `default_plan`, staged adaptive retain, multipoint fan, multi-lane ABY, `defer_expand` / `leaf_later_walk`, Express trailer fuse.
**Go deeper:** [compose.hpp](@ref dpf/compose.hpp),
[beaver.hpp](@ref dpf/beaver.hpp),
[sink_exchange.hpp](@ref dpf/net/sink_exchange.hpp),
[Application sketches](@ref applications),
[guided tour](@ref tour_party).