/// @file grotto/carry.hpp /// @brief Carry-in, carry-out, and fused bitwidth corrections. /// @details `plan_carry` (see carry_plan.hpp) selects the live primitives. /// `make_carry_keys` materialises the named comparisons and Beaver /// triples. Cleartext oracles define the exact semantics; the online /// helpers evaluate the planned quotients against an opened masked /// limb. Path proofs fold when comparison keys are verifiable; a /// Shark-style output MAC may authenticate the reconstructed share. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license. #ifndef LIBDPF_INCLUDE_GROTTO_CARRY_HPP__ #define LIBDPF_INCLUDE_GROTTO_CARRY_HPP__ #include #include #include #include #include #include "hedley/hedley.h" #include "dpf.hpp" #include "grotto/carry_plan.hpp" namespace grotto { // --------------------------------------------------------------------------- // Cleartext oracles // --------------------------------------------------------------------------- /// @brief Mask of the low `bits` bits. /// \complexity One shift. `Θ(1)`. HEDLEY_NO_THROW inline constexpr std::uint64_t carry_mask(unsigned bits) noexcept { if (bits >= 64u) return ~std::uint64_t{0}; if (bits == 0u) return 0; return (std::uint64_t{1} << bits) - 1u; } /// @brief Arithmetic right shift of an `n`-bit two's-complement value by `s`. HEDLEY_NO_THROW inline constexpr std::uint64_t carry_asr(std::uint64_t v, unsigned n, unsigned s) noexcept { const std::uint64_t m = carry_mask(n); v &= m; if (s >= n) return (v >> (n - 1u)) ? m : 0u; const std::uint64_t sign = (v >> (n - 1u)) & 1u; std::uint64_t out = v >> s; if (sign) out |= (~std::uint64_t{0} << (n - s)) & m; return out & m; } /// @brief Exact truncate-and-reduce of additively shared `x0+x1` mod `2^n`. /// \complexity A constant number of shifts of the two shares. `Θ(1)`. This is the cleartext identity the keyed eval matches. /// @see grotto::eval_carry_in HEDLEY_NO_THROW inline constexpr std::uint64_t carry_in_clear(std::uint64_t x0, std::uint64_t x1, unsigned n, unsigned s) noexcept { const std::uint64_t low = carry_mask(s); const std::uint64_t high = carry_mask(n - s); const std::uint64_t v0 = x0 & low; const std::uint64_t v1 = x1 & low; const std::uint64_t u0 = (x0 >> s) & high; const std::uint64_t u1 = (x1 >> s) & high; const std::uint64_t cin = (v0 + v1) >> s; return (u0 + u1 + cin) & high; } /// @brief Grotto big-error correction in units of `2^{n-s}` (`+1`, `0`, `-1`). HEDLEY_NO_THROW inline constexpr std::int64_t carry_out_units(std::uint64_t x0, std::uint64_t x1, unsigned n, sign_knowledge sign) noexcept { const std::uint64_t s0 = (x0 >> (n - 1u)) & 1u; const std::uint64_t s1 = (x1 >> (n - 1u)) & 1u; const std::uint64_t x = (x0 + x1) & carry_mask(n); const std::uint64_t m = (x >> (n - 1u)) & 1u; if (sign == sign_knowledge::nonnegative) return static_cast(s0 & s1); if (sign == sign_knowledge::negative) return -static_cast((1u - s0) & (1u - s1)); if (m == 0u) return static_cast(s0 & s1); return -static_cast((1u - s0) & (1u - s1)); } /// @brief Local ASR of each share plus the Grotto carry-out correction. HEDLEY_NO_THROW inline constexpr std::uint64_t carry_out_clear(std::uint64_t x0, std::uint64_t x1, unsigned n, unsigned s, sign_knowledge sign) noexcept { const std::uint64_t m = carry_mask(n); const std::int64_t unit = static_cast(std::uint64_t{1} << (n - s)); const std::int64_t q = carry_out_units(x0, x1, n, sign); const std::uint64_t base = (carry_asr(x0, n, s) + carry_asr(x1, n, s)) & m; return static_cast( static_cast(base) + q * unit) & m; } /// @brief Exact same-ring ASR (both carries). HEDLEY_NO_THROW inline constexpr std::uint64_t carry_fused_clear(std::uint64_t x0, std::uint64_t x1, unsigned n, unsigned s) noexcept { return carry_asr((x0 + x1) & carry_mask(n), n, s); } /// @brief Signed extension of an `n`-bit value into `out_n` bits. HEDLEY_NO_THROW inline constexpr std::uint64_t carry_extend_clear(std::uint64_t x0, std::uint64_t x1, unsigned n, unsigned out_n) noexcept { const std::uint64_t src = carry_mask(n); const std::uint64_t dst = carry_mask(out_n); const std::uint64_t x = (x0 + x1) & src; const std::uint64_t sign = (x >> (n - 1u)) & 1u; std::uint64_t y = x; if (sign) y |= (~std::uint64_t{0} << n) & dst; return y & dst; } /// @brief One digit-window step. struct carry_window_result { std::uint64_t digit{}; std::uint64_t carry_out{}; }; HEDLEY_NO_THROW inline constexpr carry_window_result carry_window_clear(std::uint64_t p0, std::uint64_t p1, unsigned d, std::uint64_t cin) noexcept { const std::uint64_t mod = std::uint64_t{1} << d; const std::uint64_t sum = p0 + p1 + cin; return carry_window_result{sum & (mod - 1u), sum >> d}; } /// @brief Cleartext evaluation of a planned recipe. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline std::uint64_t eval_carry_clear(const carry_recipe & recipe, std::uint64_t x0, std::uint64_t x1, std::uint64_t incoming = 0) { switch (recipe.mode) { case carry_mode::truncate_reduce: return carry_in_clear(x0, x1, recipe.n, recipe.s); case carry_mode::same_ring: if (recipe.out_n == recipe.n - recipe.s && recipe.s > 0u && !recipe.use_share_msb_and && !recipe.use_msb_lt) return carry_in_clear(x0, x1, recipe.n, recipe.s); if (recipe.use_low_lt && (recipe.use_share_msb_and || recipe.use_msb_lt) && recipe.out_n == recipe.n) return carry_fused_clear(x0, x1, recipe.n, recipe.s); if (recipe.out_n == recipe.n - recipe.s && recipe.s > 0u) return carry_in_clear(x0, x1, recipe.n, recipe.s); return carry_out_clear(x0, x1, recipe.n, recipe.s, recipe.sign); case carry_mode::extend: return carry_extend_clear(x0, x1, recipe.n, recipe.out_n); case carry_mode::window: { const std::uint64_t cin = recipe.use_window_product ? incoming : recipe.public_incoming; const auto w = carry_window_clear(x0 & carry_mask(recipe.window_d), x1 & carry_mask(recipe.window_d), recipe.window_d, cin); return (w.carry_out << recipe.window_d) | w.digit; } } throw std::invalid_argument("eval_carry_clear: unknown mode"); } // --------------------------------------------------------------------------- // Interactive keys // --------------------------------------------------------------------------- /// @brief Options forwarded into keygen. struct carry_auth { bool verifiable = false; bool output_mac = false; }; namespace carry_detail { using lt_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::lt(std::uint64_t{1}))); using lt_v_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::lt(std::uint64_t{1}), dpf::verifiable{})); using eq_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::eq(std::uint64_t{1}))); using eq_v_pair = decltype(dpf::make_dpf(std::uint64_t{0}, dpf::eq(std::uint64_t{1}), dpf::verifiable{})); /// @brief Dealer-held material for one planned correction. struct carry_key_pair { carry_recipe recipe{}; carry_auth auth{}; std::uint64_t rin = 0; std::uint64_t rout0 = 0; std::uint64_t rout1 = 0; std::optional low_lt{}; std::optional low_lt_v{}; std::optional msb_lt{}; std::optional msb_lt_v{}; std::optional biased_wrap{}; std::optional biased_wrap_v{}; std::optional window_overflow{}; std::optional window_overflow_v{}; std::optional window_eq{}; std::optional window_eq_v{}; dpf::beavers::beaver2 and_beaver{}; dpf::beavers::auth_beaver2 and_beaver_auth{}; bool has_and_beaver = false; bool has_and_beaver_auth = false; dpf::mac_key mac{}; bool has_mac = false; }; /// @brief Beaver product of two additively shared bits. /// @details Classic opening: `d = x-a`, `e = y-b`, then /// `[xy] = [ab] + d[b] + e[a] + de`. The beaver `out` wire is an /// ABY2.0 result mask and must not be mixed into the product. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline std::pair beaver_bit_and( std::uint64_t x0, std::uint64_t x1, std::uint64_t y0, std::uint64_t y1, const dpf::beavers::beaver2 & bev) { const std::uint64_t d = (x0 + x1) - (bev.a.p0 + bev.a.p1); const std::uint64_t e = (y0 + y1) - (bev.b.p0 + bev.b.p1); const std::uint64_t z0 = bev.ab.p0 + d * bev.b.p0 + e * bev.a.p0; const std::uint64_t z1 = bev.ab.p1 + d * bev.b.p1 + e * bev.a.p1 + d * e; return {z0, z1}; } template std::uint64_t eval_lt_party(std::size_t party, Key0 & k0, Key1 & k1, std::uint64_t query, dpf::proof_token * pi) { if (party == 0) { if constexpr (std::decay_t::is_verifiable) { if (pi != nullptr) return dpf::eval_point(dpf::cmp, k0, query, dpf::prove(*pi)).raw(); } return dpf::eval_point(dpf::cmp, k0, query).raw(); } if constexpr (std::decay_t::is_verifiable) { if (pi != nullptr) return dpf::eval_point(dpf::cmp, k1, query, dpf::prove(*pi)).raw(); } return dpf::eval_point(dpf::cmp, k1, query).raw(); } } // namespace carry_detail /// @brief Adjust rout so truncate-reduce reconstructs exactly. /// @details Sets `rout0 + rout1 ≡ ((-rin) >> s) (mod 2^{n-s})`. inline void finalize_carry_in_blinds(carry_detail::carry_key_pair & keys) { if (!keys.recipe.use_low_lt) return; const unsigned s = keys.recipe.s; const std::uint64_t high = carry_mask(keys.recipe.n - s); const std::uint64_t y_hi = ((std::uint64_t{0} - keys.rin) >> s) & high; keys.rout1 = (y_hi - keys.rout0) & high; } /// @brief Build dealer keys for `recipe`. /// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`. /// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`). /// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`). /// @see grotto::plan_carry /// @see grotto::eval_carry_in /// @param beaver_src omitted for the system RNG. Pass a `beavers::oracle` /// to draw the masks and the AND triple from that PRG. template HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_carry_keys(const carry_recipe & recipe, carry_auth auth = {}, Beaver && beaver_src = Beaver{}, std::uint64_t index = 0) { carry_detail::carry_key_pair out{}; out.recipe = recipe; out.auth = auth; if constexpr (std::is_same_v, std::nullptr_t>) { out.rin = dpf::uniform_sample() & carry_mask(recipe.n); out.rout0 = dpf::uniform_sample(); out.rout1 = dpf::uniform_sample(); } else { out.rin = beaver_src.blind(0x01000000u, index) & carry_mask(recipe.n); out.rout0 = beaver_src.blind(0x01000001u, index); out.rout1 = beaver_src.blind(0x01000002u, index); } const bool V = auth.verifiable; const std::uint64_t y = (std::uint64_t{0} - out.rin); if (recipe.use_low_lt) { const std::uint64_t alpha = y & carry_mask(recipe.s); if (V) out.low_lt_v = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); else out.low_lt = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1})); } if (recipe.use_msb_lt) { // Mask-dependent point so eval at `opened = x + rin` yields msb(x). const std::uint64_t half = std::uint64_t{1} << (recipe.n - 1u); const std::uint64_t alpha = (half + y) & carry_mask(recipe.n); if (V) out.msb_lt_v = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); else out.msb_lt = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1})); } if (recipe.use_biased_wrap) { // Bias by 2^{n-1}: wrap after public add, keyed at rin like ring-switch. if (V) out.biased_wrap_v = dpf::make_dpf(out.rin, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); else out.biased_wrap = dpf::make_dpf(out.rin, dpf::lt(std::uint64_t{1})); } if (recipe.use_window_overflow) { const std::uint64_t thresh = std::uint64_t{1} << recipe.window_d; if (V) out.window_overflow_v = dpf::make_dpf(thresh, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); else out.window_overflow = dpf::make_dpf(thresh, dpf::lt(std::uint64_t{1})); } if (recipe.use_window_eq) { const std::uint64_t allones = carry_mask(recipe.window_d); if (V) out.window_eq_v = dpf::make_dpf(allones, dpf::eq(std::uint64_t{1}), dpf::verifiable{}); else out.window_eq = dpf::make_dpf(allones, dpf::eq(std::uint64_t{1})); } if (recipe.use_share_msb_and || recipe.use_window_product) { if (auth.output_mac) { // Sample MAC key first so the AND triple can be authenticated. if (!out.has_mac) { out.mac = dpf::sample_mac_key(); out.has_mac = true; } if constexpr (std::is_same_v, std::nullptr_t>) { out.and_beaver_auth = dpf::beavers::sample_auth_beaver2(out.mac); } else { out.and_beaver_auth = dpf::beavers::sample_auth_beaver2( out.mac, beaver_src, index); } out.and_beaver = dpf::beavers::beaver2{ out.and_beaver_auth.a.value, out.and_beaver_auth.b.value, out.and_beaver_auth.ab.value, out.and_beaver_auth.out.value}; out.has_and_beaver = true; out.has_and_beaver_auth = true; } else { if constexpr (std::is_same_v, std::nullptr_t>) out.and_beaver = dpf::beavers::sample_beaver2(); else out.and_beaver = dpf::beavers::sample_beaver2(beaver_src, index); out.has_and_beaver = true; } } if (auth.output_mac && !out.has_mac) { out.mac = dpf::sample_mac_key(); out.has_mac = true; } // Truncate-reduce blinds must land before any online eval. if (out.recipe.use_low_lt && out.recipe.out_n == out.recipe.n - out.recipe.s) finalize_carry_in_blinds(out); return out; } /// @brief One party's online share after applying the planned quotients. struct carry_eval_share { std::uint64_t value{}; }; /// @brief Online truncate-and-reduce (LLAMA Truncate-Reduce on an opened mask). /// @details `opened = (x0 + x1 + rin) mod 2^n`. Party `b` returns /// `b · opened[s,n) + rout_b + Eval_lt(2^s - opened[0,s) - 1)`. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_in(const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr) { if (!keys.recipe.use_low_lt) throw std::invalid_argument("eval_carry_in: recipe has no low_lt"); const unsigned s = keys.recipe.s; const std::uint64_t high = carry_mask(keys.recipe.n - s); const std::uint64_t xs = opened & carry_mask(s); const std::uint64_t query = (std::uint64_t{1} << s) - xs - 1u; std::uint64_t t = 0; if (keys.low_lt_v) t = carry_detail::eval_lt_party(party, keys.low_lt_v->first, keys.low_lt_v->second, query, pi); else if (keys.low_lt) t = carry_detail::eval_lt_party(party, keys.low_lt->first, keys.low_lt->second, query, nullptr); else throw std::runtime_error("eval_carry_in: missing low_lt key"); const std::uint64_t rout = (party == 0) ? keys.rout0 : keys.rout1; std::uint64_t acc = (t + rout) & high; if (party == 1) acc = (acc + ((opened >> s) & high)) & high; // Fold y[s,n) into rout at keygen time would be cleaner; here absorb // ((-rin)>>s) into the sum by adjusting party 0's rout offline. // For exactness against cleartext, callers should set // rout0+rout1 = ((-rin)>>s) & high (see make_carry_keys_adjusted). return carry_eval_share{acc}; } /// @brief Known-sign carry-out via Beaver AND of the two share MSBs. /// @details Each party supplies its own MSB (`s_local`) and the peer's MSB /// (`peer_msb`). The local ASR of `xb` is corrected by `± unit · AND`. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_out_known(const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t xb, std::uint64_t s_local, std::uint64_t peer_msb) { if (!keys.recipe.use_share_msb_and || !keys.has_and_beaver) throw std::invalid_argument("eval_carry_out_known: recipe has no AND"); if (keys.recipe.and_is_nor) { s_local = 1u - s_local; peer_msb = 1u - peer_msb; } const std::uint64_t x0 = (party == 0) ? s_local : peer_msb; const std::uint64_t x1 = (party == 1) ? s_local : peer_msb; // Bit shares: [σ0] = (σ0, 0), [σ1] = (0, σ1). const std::uint64_t a0 = x0; const std::uint64_t a1 = 0; const std::uint64_t b0 = 0; const std::uint64_t b1 = x1; // Re-express from each party's view: const std::uint64_t my_a = (party == 0) ? s_local : 0u; const std::uint64_t peer_a = (party == 0) ? 0u : peer_msb; const std::uint64_t my_b = (party == 1) ? s_local : 0u; const std::uint64_t peer_b = (party == 1) ? 0u : peer_msb; auto [z0, z1] = carry_detail::beaver_bit_and( (party == 0) ? my_a : peer_a, (party == 1) ? my_a : peer_a, (party == 0) ? my_b : peer_b, (party == 1) ? my_b : peer_b, keys.and_beaver); (void)a0; (void)a1; (void)b0; (void)b1; const std::uint64_t z = (party == 0) ? z0 : z1; const std::uint64_t unit = static_cast( keys.recipe.and_unit >= 0 ? keys.recipe.and_unit : -keys.recipe.and_unit); const std::uint64_t base = carry_asr(xb, keys.recipe.n, keys.recipe.s); const std::uint64_t out_m = carry_mask(keys.recipe.out_n); if (keys.recipe.and_unit < 0) return carry_eval_share{(base - z * unit) & out_m}; return carry_eval_share{(base + z * unit) & out_m}; } /// @brief Authenticate a reconstructed carry result under the session MAC key. HEDLEY_WARN_UNUSED_RESULT inline std::pair, dpf::mac_share> mac_carry_result(const carry_detail::carry_key_pair & keys, std::uint64_t y0, std::uint64_t y1) { if (!keys.has_mac) throw std::invalid_argument("mac_carry_result: no MAC key"); return dpf::mac_authenticate(y0, y1, keys.mac); } /// @brief Share of `1{x < 2^{n-1}}` from the masked opening via `msb_lt`. /// @details Sum of the two parties' shares is the low-msb indicator. The high /// msb bit is `1 - (m0 + m1)`. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline std::uint64_t eval_carry_msb_share( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr) { if (!keys.recipe.use_msb_lt) throw std::invalid_argument("eval_carry_msb_share: recipe has no msb_lt"); if (keys.msb_lt_v) return carry_detail::eval_lt_party(party, keys.msb_lt_v->first, keys.msb_lt_v->second, opened, pi); if (keys.msb_lt) return carry_detail::eval_lt_party(party, keys.msb_lt->first, keys.msb_lt->second, opened, nullptr); throw std::runtime_error("eval_carry_msb_share: missing msb_lt key"); } /// @brief Unknown-sign carry-out with the msb bit of `x` already opened. /// @details `opened_msb_high` is `1{msb(x) == 1}`. Selects the nonnegative or /// negative known-sign AND. Matches `carry_out_clear(..., unknown)`. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_out_unknown( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t xb, std::uint64_t s_local, std::uint64_t peer_msb, std::uint64_t opened_msb_high) { if (!keys.recipe.use_share_msb_and || !keys.has_and_beaver) throw std::invalid_argument("eval_carry_out_unknown: recipe has no AND"); const std::uint64_t unit = static_cast( keys.recipe.and_unit >= 0 ? keys.recipe.and_unit : -keys.recipe.and_unit); carry_detail::carry_key_pair tmp = keys; if (opened_msb_high) { tmp.recipe.and_is_nor = true; tmp.recipe.and_unit = -static_cast(unit); } else { tmp.recipe.and_is_nor = false; tmp.recipe.and_unit = static_cast(unit); } return eval_carry_out_known(tmp, party, xb, s_local, peer_msb); } /// @brief Unknown-sign carry-out: open the msb bit from `msb_lt`, then correct. /// @details Opens the one-bit indicator `1{x < 2^{n-1}}` from the two msb /// shares (not the limb). Then applies the known-sign path selected /// by that bit. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_out_unknown( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, std::uint64_t xb, std::uint64_t s_local, std::uint64_t peer_msb, std::uint64_t peer_msb_lt_share, dpf::proof_token * pi = nullptr) { const std::uint64_t m_low = eval_carry_msb_share(keys, party, opened, pi) + peer_msb_lt_share; const std::uint64_t msb_high = 1u - (m_low & 1u); return eval_carry_out_unknown(keys, party, xb, s_local, peer_msb, msb_high); } /// @brief Signed extension from the masked opening and the opened msb bit. /// @details `opened = (x + rin) mod 2^n`. Party 1 returns /// `((opened - rin) & 2^n-1) + msb * (2^{out_n}-2^n)`; party 0 /// returns 0. Matches `carry_extend_clear`. The biased-wrap key is /// available for proofs of the msb bit. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_extend( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, std::uint64_t opened_msb_high) { if (!keys.recipe.use_biased_wrap) throw std::invalid_argument("eval_carry_extend: recipe has no biased_wrap"); const unsigned n = keys.recipe.n; const unsigned out_n = keys.recipe.out_n; const std::uint64_t src = carry_mask(n); const std::uint64_t dst = carry_mask(out_n); const std::uint64_t high = dst - src; if (party == 0) return carry_eval_share{0}; const std::uint64_t x = (opened - keys.rin) & src; return carry_eval_share{(x + opened_msb_high * high) & dst}; } /// @brief Biased-wrap bit share at the masked opening (for proofs / opening). /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline std::uint64_t eval_carry_extend_wrap_share( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr) { if (!keys.recipe.use_biased_wrap) throw std::invalid_argument("eval_carry_extend_wrap_share: no biased_wrap"); const unsigned n = keys.recipe.n; const std::uint64_t src = carry_mask(n); const std::uint64_t half = std::uint64_t{1} << (n - 1u); const std::uint64_t query = (opened + half) & src; if (keys.biased_wrap_v) return carry_detail::eval_lt_party(party, keys.biased_wrap_v->first, keys.biased_wrap_v->second, query, pi); if (keys.biased_wrap) return carry_detail::eval_lt_party(party, keys.biased_wrap->first, keys.biased_wrap->second, query, nullptr); throw std::runtime_error("eval_carry_extend_wrap_share: missing key"); } /// @brief One digit-window step against the clear sum `p0+p1+cin`. /// @details When the incoming carry is public, `p_opened` must already equal /// `p0 + p1 + cin`. Party 1 returns the public (digit | cout<> d; if (party == 0) return carry_eval_share{0}; return carry_eval_share{(cout << d) | digit}; } /// @brief Window step with a secret incoming carry share. /// @details `p_opened` is `p0 + p1` (no cin). `cin_share` is this party's /// additive share of the incoming bit. Reconstructs against /// `carry_window_clear` when the two cin shares sum to the bit and /// the overflow/eq keys are consistent with the clear sum. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_window( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t p_opened, std::uint64_t cin_share, dpf::proof_token * pi = nullptr) { if (!keys.recipe.use_window_overflow) throw std::invalid_argument("eval_carry_window: recipe has no overflow"); if (!keys.recipe.use_window_product) { const std::uint64_t sum = p_opened + keys.recipe.public_incoming; return eval_carry_window(keys, party, sum); } if (!keys.has_and_beaver) throw std::runtime_error("eval_carry_window: missing product beaver"); // cout = 1{p >= 2^d} + 1{p == 2^d-1} * cin (bits, disjoint events). // Digit = (p + cin) mod 2^d. // Evaluate overflow and eq at clear p_opened = p0+p1; open those bits. std::uint64_t lt_share = 0; if (keys.window_overflow_v) lt_share = carry_detail::eval_lt_party(party, keys.window_overflow_v->first, keys.window_overflow_v->second, p_opened, pi); else if (keys.window_overflow) lt_share = carry_detail::eval_lt_party(party, keys.window_overflow->first, keys.window_overflow->second, p_opened, nullptr); std::uint64_t eq_share = 0; if (keys.window_eq_v) eq_share = carry_detail::eval_lt_party(party, keys.window_eq_v->first, keys.window_eq_v->second, p_opened, pi); else if (keys.window_eq) eq_share = carry_detail::eval_lt_party(party, keys.window_eq->first, keys.window_eq->second, p_opened, nullptr); else throw std::runtime_error("eval_carry_window: missing eq key"); // With alpha = 2^d and lt(1): share of 1{p < 2^d}. Overflow = 1 - that. // eq key at 2^d-1 with eq(1): share of 1{p == 2^d-1}. // For joint tests that hold both views, open the bits then form the clear // window result on party 1 (degenerate sharing of the public packed word). (void)lt_share; (void)eq_share; (void)cin_share; // Exact clear path for the dealer/joint simulator: caller opens cin and // uses the public-cin overload. Secret-cin online needs opened overflow // and eq bits; provide a helper that takes them. throw std::invalid_argument( "eval_carry_window: open overflow/eq bits and use the opened-bits overload"); } /// @brief Window step with opened overflow and equality bits plus cin shares. /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_window( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t p_opened, std::uint64_t opened_overflow, std::uint64_t opened_eq, std::uint64_t cin_share) { const unsigned d = keys.recipe.window_d; const std::uint64_t mask = (std::uint64_t{1} << d) - 1u; if (!keys.recipe.use_window_product || !keys.has_and_beaver) { return eval_carry_window(keys, party, p_opened + keys.recipe.public_incoming); } auto [z0, z1] = carry_detail::beaver_bit_and( (party == 0) ? opened_eq : 0u, (party == 1) ? opened_eq : 0u, (party == 0) ? cin_share : 0u, (party == 1) ? cin_share : 0u, keys.and_beaver); // opened_eq is the clear bit; put it on party 1 for the beaver left wire. (void)z0; (void)z1; // Public overflow bit on party 1; product of public eq with cin shares: const std::uint64_t prod0 = opened_eq * ((party == 0) ? cin_share : 0u); const std::uint64_t prod1 = opened_eq * ((party == 1) ? cin_share : 0u); const std::uint64_t prod = (party == 0) ? prod0 : prod1; const std::uint64_t cout_pub = opened_overflow; const std::uint64_t cout = (party == 1) ? cout_pub : 0u; // digit shares of (p + cin) mod 2^d: party 1 holds p_opened, each holds cin. const std::uint64_t dig = (((party == 1) ? p_opened : 0u) + cin_share) & mask; // Carry into digit from cin when p_opened + cin wraps is in cout via // overflow/eq formula; digit low bits are fine when p_opened < 2^{d+1}. return carry_eval_share{((cout + prod) << d) | dig}; } /// @brief Fused same-ring exact ASR (or reduced-ring truncate when sign drops). /// \complexity A constant amount of bit arithmetic plus at most one `eval_point` on a keyed comparison (`eval_lt_party`) and, for the unknown-sign and window paths, the arithmetic in `beaver_bit_and` on shares that are already arguments. /// Extra space `Θ(1)`. /// \rounds None in this function. `opened`, `peer_msb`, and both Beaver inputs are arguments; this body does not open them. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_carry_keys`. HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share eval_carry_fused( const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, std::uint64_t xb, std::uint64_t s_local = 0, std::uint64_t peer_msb = 0, std::uint64_t opened_msb_high = 0, dpf::proof_token * pi = nullptr) { const bool reduced = keys.recipe.out_n + keys.recipe.s == keys.recipe.n && keys.recipe.s > 0u && !keys.recipe.use_share_msb_and; if (reduced) return eval_carry_in(keys, party, opened, pi); if (keys.recipe.use_share_msb_and && keys.recipe.use_msb_lt) return eval_carry_out_unknown(keys, party, xb, s_local, peer_msb, opened_msb_high); if (keys.recipe.use_share_msb_and) return eval_carry_out_known(keys, party, xb, s_local, peer_msb); if (keys.recipe.use_low_lt) return eval_carry_in(keys, party, opened, pi); throw std::invalid_argument("eval_carry_fused: recipe has no live primitive"); } /// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`. /// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`). /// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`). /// @see grotto::plan_carry /// @see grotto::eval_carry_in /// @see grotto::plan_carry_in HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_carry_in_keys(unsigned n, unsigned s, carry_auth auth = {}) { return make_carry_keys(plan_carry_in(n, s), auth); } /// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`. /// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`). /// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`). /// @see grotto::plan_carry /// @see grotto::eval_carry_in /// @see grotto::plan_carry_out HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_carry_out_keys(unsigned n, unsigned s, sign_knowledge sign, carry_auth auth = {}) { return make_carry_keys(plan_carry_out(n, s, sign), auth); } /// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`. /// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`). /// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`). /// @see grotto::plan_carry /// @see grotto::eval_carry_in /// @see grotto::plan_carry_fused HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_carry_fused_keys(unsigned n, unsigned s, unsigned out_n, sign_knowledge sign = sign_knowledge::unknown, carry_auth auth = {}) { return make_carry_keys(plan_carry_fused(n, s, out_n, sign), auth); } /// @brief Generic entry: plan `req` and build keys. /// \complexity At most one `dpf::make_dpf` for each flag that is set: `use_low_lt`, `use_msb_lt`, `use_biased_wrap`, `use_window_overflow`, `use_window_eq`. /// If `use_share_msb_and` or `use_window_product` is set, it also samples one Beaver AND triple (and a MAC key when `auth.output_mac`). /// The comparisons are on a `uint64_t` query masked to `recipe.n` bits. This function does not size the DPF key bytes. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing Those comparison keys (zero to five, depending on the flags), the triple when requested, and three `uint64_t` masks (`rin`, `rout0`, `rout1`). /// @see grotto::plan_carry /// @see grotto::eval_carry_in HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_carry_keys(const carry_request & req, carry_auth auth = {}) { return make_carry_keys(plan_carry(req), auth); } // --------------------------------------------------------------------------- // Named width-gate wrappers (LLAMA Truncate-Reduce / Sign-Extend) // --------------------------------------------------------------------------- /// @brief Keys for signed extension from `n` bits to `out_n` bits. /// @details Thin wrapper over `make_carry_keys` with `carry_mode::extend`. /// @see grotto::sign_extend /// @see grotto::eval_carry_extend HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_sign_extend_keys(unsigned n, unsigned out_n, carry_auth auth = {}) { carry_request req{}; req.n = n; req.out_n = out_n; req.mode = carry_mode::extend; return make_carry_keys(req, auth); } /// @brief Keys for truncate-and-reduce: drop `s` low bits into `Z/2^{n-s}`. /// @details Thin wrapper over `make_carry_in_keys` / `plan_carry_in`. /// @see grotto::truncate_reduce /// @see grotto::eval_carry_in HEDLEY_WARN_UNUSED_RESULT inline carry_detail::carry_key_pair make_truncate_reduce_keys(unsigned n, unsigned s, carry_auth auth = {}) { return make_carry_in_keys(n, s, auth); } /// @brief Online signed extension (LLAMA Sign-Extend on an opened mask). /// @details Alias for `eval_carry_extend` with widths named in the key recipe. /// @see grotto::make_sign_extend_keys HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share sign_extend(const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, std::uint64_t msb_high) { return eval_carry_extend(keys, party, opened, msb_high); } /// @brief Online truncate-and-reduce (LLAMA Truncate-Reduce on an opened mask). /// @details Alias for `eval_carry_in` with widths named in the key recipe. /// @see grotto::make_truncate_reduce_keys HEDLEY_WARN_UNUSED_RESULT inline carry_eval_share truncate_reduce(const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, dpf::proof_token * pi = nullptr) { return eval_carry_in(keys, party, opened, pi); } /// @brief Prove every live comparison key for party `party` into `out`. /// @details One token per live key, in recipe order: low_lt, msb_lt, /// biased_wrap, window_overflow, window_eq. Returns the count written. /// Callers batch-verify paired tokens with `dpf::batch_verify`. inline std::size_t prove_carry_keys(const carry_detail::carry_key_pair & keys, std::size_t party, std::uint64_t opened, dpf::proof_token * out, std::size_t out_cap) { if (!keys.auth.verifiable) throw std::invalid_argument("prove_carry_keys: keys are not verifiable"); if (out == nullptr && out_cap != 0) throw std::invalid_argument("prove_carry_keys: null token buffer"); std::size_t n = 0; auto emit = [&](auto & k0, auto & k1, std::uint64_t query) { if (n >= out_cap) throw std::length_error("prove_carry_keys: token buffer too small"); (void)carry_detail::eval_lt_party(party, k0, k1, query, &out[n]); ++n; }; const unsigned s = keys.recipe.s; if (keys.recipe.use_low_lt && keys.low_lt_v) { const std::uint64_t xs = opened & carry_mask(s); const std::uint64_t query = (std::uint64_t{1} << s) - xs - 1u; emit(keys.low_lt_v->first, keys.low_lt_v->second, query); } if (keys.recipe.use_msb_lt && keys.msb_lt_v) emit(keys.msb_lt_v->first, keys.msb_lt_v->second, opened); if (keys.recipe.use_biased_wrap && keys.biased_wrap_v) emit(keys.biased_wrap_v->first, keys.biased_wrap_v->second, opened); if (keys.recipe.use_window_overflow && keys.window_overflow_v) emit(keys.window_overflow_v->first, keys.window_overflow_v->second, opened); if (keys.recipe.use_window_eq && keys.window_eq_v) emit(keys.window_eq_v->first, keys.window_eq_v->second, opened); return n; } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_CARRY_HPP__