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>
6.2 KiB
A boolean function of a leaf
\htmlonly
Party 0 garbles. Party 1 evaluates. Free-XOR, half-gates ([ePrint 2014/756](@ref bib_halfgates)). One table row is 32 bytes. Outputs are XOR shares of the output bits: the garbler's share is the permute bit, the evaluator's share is the color of the label it holds.
The circuit this library is built around is the zero-key AES-MMO block
prg::aes128::eval already uses on every tree expand. A seed or a leaf
can enter that block without being opened. The same netlist is a hand-built
straight line of XOR, AND, XNOR, and NOT. Inputs are declared first.
| Leaf you hold | Into the netlist | Back out |
|---|---|---|
| Point leaf (subtractive) | b2y |
y2b |
| Comparison leaf (additive) | a2y |
y2a |
fss_share |
fss2y |
y2fss |
| Replicated, parties 0 and 1 garble | rss2y |
y2rss |
Bits are least-significant first, one byte each, 0 or 1. width 0 means
the whole ring on the way in, and the vector length on the way out.
Both parties' shares are arguments. The edaBit open of x - r and the
daBit open of b ⊕ r are resolved inside the call, the same way
edabit::a2b_gmw_pair does. Those masked values are uniform. The integer
stays shared.
rss2y does not wake party 2. Party 0 already holds x0 and x1. Party 1
already holds x2. r0.next must equal r1.own. Top-level dpf::rss2y
and dpf::y2rss are the local (3,3) casts and are different functions.
auto [k0, k1] = dpf::make_dpf(std::uint8_t{42}, std::uint32_t{0x6b});
auto s0 = *dpf::eval_point(k0, std::uint8_t{42});
auto s1 = *dpf::eval_point(k1, std::uint8_t{42});
auto [y0, y1] = dpf::yao::b2y(s0, s1, 8);
dpf::yao::netlist n;
dpf::yao::bit in[8];
for (int i = 0; i < 8; ++i)
in[i] = n.shared_in();
n.out(n.and_(in[0], in[1]));
dpf::yao::session garbler;
auto out0 = garbler.eval(0, n, y0.data(), link); // party 1 passes y1
auto [z0, z1] = dpf::yao::y2b<std::uint32_t>(out0, out1, 1);
// reconstruct(z0, z1) == (0x6b & 1) & ((0x6b >> 1) & 1)
session keeps the IKNP base OT. The first eval that needs a choice
label runs Chou–Orlandi. Later evals on that session only extend. Do not
interleave iknp::sample on the same channel. Party 0 is the garbler for
the life of the session. Tables are one-time. The model is semi-honest.
What stays a key
A public query against a secret point is a comparison key. An interval is
an interval key. A polynomial in a public offset is Grotto, one comparison
and a local dot. A product of two leaf shares is a Beaver triple. A word
mux is one bit×ring inject. cost_pass still chooses among a DCF mask, an
edaBit MSB, and a full adder for those.
Garble when the AND depth is the cost and the circuit is this shape: the
MMO block (5120 ANDs, one message, about 160 KiB of tables), the AES-128
block under a shared key (6400 ANDs, key schedule included), or a netlist
you built because the leaf bits are the input. The Boyar–Peralta S-box
inside those blocks is 32 ANDs. correction_level is that hash as one garble: eight MMO blocks, lanes
0..3 on each seed, 40960 ANDs, XOR shares of the four-block digest.
The level and the prefix share are packed into the inputs
(pack_correction_level); the netlist itself does not change per level.
party/oblivious_hash.hpp still walks the same S-box as GMW AND layers,
80 opens per level.
A secret branch
The netlist above is a straight line. A secret if/else, or a secret choice
among a few blocks, still belongs on that leaf circuit: the bits came from
b2y / a2y / rss2y, and the answer goes back with y2b / y2a /
y2rss. It is not a reason to open the leaf.
yao::eval_if stacks the two branches
([Heath and Kolesnikov, CRYPTO 2020](@ref bib_stacked)). Each branch is
garbled from the hash of the control label for that semantic bit. The
generator XORs the materials. The evaluator rebuilds the inactive branch
from a seed under the control label and XORs it out. Transmitted AND rows
follow the heavier branch, plus four translation rows per output bit so the
garbler's share does not depend on the branch.
yao::eval_one_hot is the same stack for k netlists, k from 2 to 8
([Heath and Kolesnikov, CCS 2021](@ref bib_onehot)). The index is
index_p0 XOR index_p1. The demux row for the selector color carries the
inactive seeds.
Both parties pass the same netlists. Branch inputs use the same layout as
eval_pair: a shared entry is that party's XOR share, priv0 is read on
party 0, priv1 on party 1. The reconstructed bit is share0[i] XOR share1[i].
stack_blocks is the stacked material. naive_blocks is the sum of the
branches. A comparison or a public-offset polynomial still stays on the key.
Calls
| Call | What it does |
|---|---|
yao::netlist |
XOR, AND, XNOR, NOT, XOR with a public bit. n_and() is the row count |
yao::eval_plain |
The same wires in the clear |
yao::eval_local / eval_pair |
Garble and evaluate in one process |
yao::session::eval |
Garble on the peer channel. Party 0 sends the tables |
yao::a2y b2y fss2y rss2y |
Ring share to LSB-first XOR bits |
yao::y2a y2b y2fss y2rss |
Those bits back to a ring share |
yao::aes_mmo |
One zero-key MMO block on a session. pos is public |
yao::correction_level |
Eight of those blocks: the correction-seed hash for one tree level |
yao::aes128 |
AES-128 of a shared block under a shared key |
yao::eval_if |
Stacked if/else. Rows follow the heavier branch ([CRYPTO 2020](@ref bib_stacked)) |
yao::eval_one_hot |
One stack over k branches ([CCS 2021](@ref bib_onehot)) |
Go deeper: [a secret branch](@ref yao_stack), [yao.hpp](@ref dpf/yao.hpp), [yao_stack.hpp](@ref dpf/yao_stack.hpp), [yao_share.hpp](@ref dpf/yao_share.hpp), [yao_aes.hpp](@ref dpf/yao_aes.hpp), [F_Yao](@ref yao.hpp), [F_YaoShare](@ref yao_share.hpp), [half-gates](@ref bib_halfgates), [edaBits](@ref bib_edabits). The walk that still uses GMW for the hash is [the dealer-free tour](@ref tour_ds).