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

@ -12,6 +12,10 @@ The same offset also drives [offset Horner](@ref offset_horner),
## Binomial jet {#offset_jet}
\htmlonly
<div class="eli5"><b>ELI5.</b> The dealer keys the binomial coefficients of (center + eta) up to a chosen degree. After eta is public, a dot with those shares is the monomial or the polynomial, with no further tree walk.</div>
\endhtmlonly
`make_offset_jet_keys(center, degree)` keys one incremental `gt` whose
payload is the vector of \f$\binom{\mathrm{center}}{k}\f$ in
\f$\mathbb{Z}/2^{64}\f$. After `eta` opens,
@ -61,6 +65,10 @@ one reciprocal after the shares are opened. Those are not separate APIs.
## Exact ring switch {#ring_switch}
\htmlonly
<div class="eli5"><b>ELI5.</b> An n-bit limb is rewritten into another modulus, a field, or a P-256 scalar by an exact map on the opened residue. The value does not go through floating point, and the map does not expand another key.</div>
\endhtmlonly
For an unsigned \f$n\f$-bit limb (\f$n\le 64\f$) with representatives in
\f$[0,2^n)\f$,
@ -117,6 +125,10 @@ See also [representation shift and twisted jets](@ref repr_and_twist).
## Offset Horner {#offset_horner}
\htmlonly
<div class="eli5"><b>ELI5.</b> Powers of a public center are already shared. Shifting them by the opened eta, the binomial way, evaluates the polynomial at the secret. The degree here is fixed in the template.</div>
\endhtmlonly
`make_offset_horner_keys<Input, Degree>(center)` keys one `gt` whose
payload is `center^m` for `m = 0 .. Degree`. `Degree` is at most 3
(`offset_horner_max_degree`). Pass `dpf::verifiable{}` for proof tokens.
@ -147,6 +159,10 @@ reusable key.
## Offset polynomial {#offset_poly}
\htmlonly
<div class="eli5"><b>ELI5.</b> Same shift as offset Horner, but the degree is an argument, so the number of powered shares is chosen when the keys are built.</div>
\endhtmlonly
`make_offset_poly_keys(center, degree)` is offset Horner at a runtime
degree, at most 16 (`offset_poly_max_degree`). One incremental `gt`
whose payload is the vector of powers. `offset_poly_eval<Party>` dots the shifted powers.
@ -176,6 +192,10 @@ auto opened = s0 + grotto::offset_poly_eval<1>(mat, knots, coeff, eta);
## Carry {#carry}
\htmlonly
<div class="eli5"><b>ELI5.</b> A carry across a shift is a short list of comparisons and bit corrections, not a generic circuit. The request names the source width, the shift, and the width of what comes out. Truncate, arithmetic shift, and sign-extend are the same plan with different output widths.</div>
\endhtmlonly
A `carry_request` names the source width `n`, the shift `s`, the output
width `out_n`, a `carry_mode` (`truncate_reduce`, `same_ring`, `extend`,
`window`), and a `sign_knowledge` (`unknown`, `nonnegative`, `negative`).
@ -210,6 +230,10 @@ auto clear = grotto::eval_carry_clear(keys.recipe, x0, x1);
## Prefix parity {#prefix_parity}
\htmlonly
<div class="eli5"><b>ELI5.</b> One walk of an existing key stops at the public endpoints and folds XOR or addition along that prefix. The fold is O(number of endpoints), not a new key per prefix.</div>
\endhtmlonly
`prefix_parities(key, endpoints)` walks a key to the sorted endpoints and
returns XOR shares of the prefix parities, plus the index of the first
endpoint on the wrap. `segment_parities` turns those into one share per
@ -241,6 +265,67 @@ auto signs = grotto::signed_prefix_parities(cmp0, ends);
**Defined in**\n
@ref grotto/prefix_parity.hpp
## Several LUTs, one comparison {#lut_union}
\htmlonly
<div class="eli5"><b>ELI5.</b> Stack the breakpoints of every table into one sorted list. One comparison and one prefix walk label the pieces of that list. Each table then sums the labels that fall inside its own intervals.</div>
\endhtmlonly
`make_lut_union_plan(luts, eta)` shifts every piecewise LUT by the public
`eta`, inserts the same domain-minimum and carry cuts as
[offset polynomial](@ref offset_poly), and sorts the union. A piece of one
LUT is a span of those union knots: `[begin, end)`, or
`[begin, end-of-union) ∪ [0, end)` when the piece wraps. The span stores
that piece's binomial shift by its public `kappa`. Spans of one LUT
partition the union.
The interactive plan is one comparison, whatever the number of LUTs and
whatever the number of union knots:
- `plan.comparisons` and `plan.prefix_walks` are 1.
- `plan.depth` and `plan.geneval_rounds()` are the bitlength of the input.
- `plan.degree` is the widest polynomial. The payload is
`1, center, …, center^degree`.
- `schedule_lut_union` records one `fss_cmp` of that depth. The slot is
`lut_union_slot_bytes`: one AES block, or `lanes * 8` when the power
vector is wider. Prefix parity of the union is local after that
comparison.
`lut_union_eval<Party>` reads one `make_offset_poly_keys` key of degree
at least `plan.degree` and returns a share per LUT. `geneval_lut_union`
opens that comparison from XOR shares of the center, as in
`geneval_offset_horner`. Pass `dpf::arith_input` when the shares add to
the center in the input group.
`piecewise_from_easy` and `piecewise_from_constant` adapt the cleartext
tables. An `easy_lut` denominator other than 1 is a rounding division, so
`piecewise_from_easy` rejects it. Powers that are zero on every piece are
dropped, and the shared payload stays only as wide as the widest remaining
degree.
\code{cpp}
grotto::piecewise_lut<std::uint8_t> low{{0, 10}, {{1, 0}, {0, 2}}};
grotto::piecewise_lut<std::uint8_t> high{{0, 4, 12}, {{3, 0}, {1, 1}, {9, 4}}};
const std::uint8_t center = 12;
const std::uint8_t eta = 3;
auto plan = grotto::make_lut_union_plan({low, high}, eta);
auto mat = grotto::make_offset_poly_keys<std::uint8_t>(center, plan.degree);
auto s0 = grotto::lut_union_eval<0>(mat, plan);
auto s1 = grotto::lut_union_eval<1>(mat, plan);
dpf::protocol::composer composer(0);
grotto::schedule_lut_union(composer, plan); // rounds == plan.depth
\endcode
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">lut_union.cpp</b> \include{cpp} grotto/lut_union.cpp
</div>
**Defined in**\n
@ref grotto/lut_union.hpp
## Cleartext maps {#grotto_luts}
These functions take a raw fixed-point word (`n << fractional_bits`) and
@ -250,6 +335,10 @@ return a raw word. They do not build a DPF. The type
## Fixed-point product {#fixedpoint_mul}
\htmlonly
<div class="eli5"><b>ELI5.</b> The product lives in a ring wide enough for both fixed-point operands. One Beaver triple in that ring is the product; the binary point is placed by a public shift afterward.</div>
\endhtmlonly
`fixed_mul<IntegerBits, FractionalBits>(lhs, rhs)` multiplies two
`fixedpoint` values and keeps that many integer bits (including the sign)
and fraction bits. Bits below the fraction are floored. The product type
@ -267,6 +356,10 @@ auto prod = grotto::fixed_mul<16, 16>(q16{1.5}, q16{2.0});
## Lookup tables {#lookup_tables}
\htmlonly
<div class="eli5"><b>ELI5.</b> A cleartext approximation is replaced by a table addressed with the secret. Constant, easy, range, window, and principal tables differ in how many bits of the input they consume and how the correction is added.</div>
\endhtmlonly
Constant, easy, principal, range, and window tables are included from
`grotto.hpp`. The dyadic table comes in through `exact_steps.hpp`, which
`grotto.hpp` also includes.
@ -303,6 +396,8 @@ Constant, easy, principal, range, and window tables are included from
(`principal_precision`). Names: `ln`, `exp`, `sin`, `tanf`, `tang`,
`sinh`, `cosh`, `sqrt`, `coth`, `sec`, `gsec`, `csch`, `inv`, `rsqrt`,
`invsq`.
- **Wavelet.** Haar and bior(5,3) compressed tables:
[Wavelet lookup tables](@ref dwt_luts).
\code{cpp}
auto sign = grotto::make_exact_constant_lut<std::int32_t>(
@ -368,6 +463,69 @@ picks the piece and calls that Horner step.
@ref grotto/window_lut.hpp, @ref grotto/principal_lut.hpp,
@ref grotto/piecewise.hpp
## Wavelet lookup tables {#dwt_luts}
\htmlonly
<div class="eli5"><b>ELI5.</b> A wavelet step is a fixed linear combination. The LUT stores that combination so the signal stays in shares and never enters a floating-point routine. Haar and biorthogonal 5/3 are the two filters.</div>
\endhtmlonly
`make_haar_dwt_lut` and `make_bior53_dwt_lut` compress a real signal of
length \f$2^n\f$ and evaluate it as a fixed-point word. The construction
is the cleartext Haar and bior(5,3) lookup of Reis, Ugurbil, Wagh, Henry,
and de Vega, [ePrint 2025/013](@ref bib_wave), Equations (7) and (8).
`sample_dwt_signal(domain_bits, fractional_bits, f)` writes the grid
\f$i \cdot 2^{-f}\f$ for \f$i \in [0, 2^n)\f$.
Both builders run the depth-\f$j\f$ low-pass with the smooth edge
extension used for that paper's accuracy tables. PyWavelets calls these
filters `haar` and `bior2.2`; bior(5,3) is the same pair, named there by
vanishing moments. Building either table is \f$\Theta(N)\f$ arithmetic
and extra memory, \f$N = 2^n\f$.
Haar then multiplies the approximation coefficients by \f$2^{-j/2}\f$
and rounds down to \f$f\f$ fraction bits. On this grid that coefficient
is the mean of each block of \f$2^j\f$ samples. Evaluation reads
`coeff[raw >> j]`, one indexing step, \f$\Theta(1)\f$.
bior(5,3) multiplies by \f$2^{j/2}\f$ and rounds down the same way.
Smooth extension prepends two coefficients, so the bin `msb = raw >> j`
lives at index `msb + 2`, and the next tap at `msb + 3`, wrapping in the
stored vector. With `lsb = raw mod 2^j`,
\f[
y = \bigl\lfloor\bigl(c_{\mathrm{msb}+2}\,(2^j - \mathrm{lsb})
+ c_{\mathrm{msb}+3}\,\mathrm{lsb}\bigr) / 2^{2j}\bigr\rfloor.
\f]
That is Equation (8): the Lemma 6 weights \f$(2^j - \mathrm{lsb}_j)\f$
and \f$\mathrm{lsb}_j\f$, in integer arithmetic. Two multiplications and
a shift, \f$\Theta(1)\f$.
The paper's online protocols look these tables up under a DPF. Haar is
paired there with a deterministic Pika truncation; bior(5,3) is paired
with segment parity. Those protocols are not a key type here. The value
they open is `table(raw)`.
\code{cpp}
auto samples = grotto::sample_dwt_signal(6, 4, [](double x) {
return 1.0 / (1.0 + std::exp(-(x - 2.0)));
});
auto haar = grotto::make_haar_dwt_lut(samples, 4, 2);
auto bior = grotto::make_bior53_dwt_lut(samples, 4, 2);
auto h = haar(std::uint64_t{32});
auto b = bior(std::uint64_t{33});
\endcode
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">dwt_lut.cpp</b> \include{cpp} grotto/dwt_lut.cpp
</div>
**Defined in**\n
@ref grotto/dwt_lut.hpp
## Closed form {#closed_form}
`eval_closed(closed::atanh, fractional_bits, raw)` composes
@ -423,3 +581,7 @@ for a functor type.
**Defined in**\n
@ref grotto/gadgets.hpp, @ref grotto/gadget_hints.hpp
\htmlonly
<div class="tldr"><b>TL;DR.</b> Open eta = x − r once. Jets and Horner turn that public distance into polynomial powers. Ring switch and carry move the integer. Prefix parity folds a key you already hold. Lookup tables, including the wavelet tables, replace cleartext math. A fixed-point product is one Beaver triple.</div>
\endhtmlonly