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>
283 lines
9.6 KiB
C++
283 lines
9.6 KiB
C++
/// @file grotto/carry_plan.hpp
|
|
/// @brief Pure planner for carry-in, carry-out, and fused bitwidth corrections.
|
|
/// @details A request names the width change and what is known about the sign.
|
|
/// The recipe lists only the primitives that are still live after the
|
|
/// output modulus and a public incoming carry have killed the rest.
|
|
/// No keys are allocated here.
|
|
/// @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_PLAN_HPP__
|
|
#define LIBDPF_INCLUDE_GROTTO_CARRY_PLAN_HPP__
|
|
|
|
#include <cstdint>
|
|
#include <stdexcept>
|
|
|
|
#include "hedley/hedley.h"
|
|
|
|
namespace grotto
|
|
{
|
|
|
|
/// @brief Kind of bitwidth change the caller wants.
|
|
enum class carry_mode : std::uint8_t
|
|
{
|
|
/// Drop `s` low bits; result lives in `Z/2^{n-s}`. Only carry-in survives.
|
|
truncate_reduce = 0,
|
|
/// Arithmetic right shift that stays in `Z/2^n`. Carry-out (sign wrap).
|
|
same_ring = 1,
|
|
/// Signed extension from `n` bits to `out_n` bits. Biased wrap only.
|
|
extend = 2,
|
|
/// Digit / window of width `s` with an optional incoming carry.
|
|
window = 3
|
|
};
|
|
|
|
/// @brief What the caller already knows about `msb(x)`.
|
|
enum class sign_knowledge : std::uint8_t
|
|
{
|
|
unknown = 0,
|
|
nonnegative = 1,
|
|
negative = 2
|
|
};
|
|
|
|
/// @brief Inputs to the planner. Widths are in bits.
|
|
struct carry_request
|
|
{
|
|
/// @brief Source ring bitlength.
|
|
unsigned n = 0;
|
|
/// @brief Truncation / window width. Unused for plain `extend`.
|
|
unsigned s = 0;
|
|
/// @brief Output ring bitlength.
|
|
unsigned out_n = 0;
|
|
/// @brief Which width change to plan.
|
|
carry_mode mode = carry_mode::truncate_reduce;
|
|
/// @brief Sign knowledge for same-ring carry-out.
|
|
sign_knowledge sign = sign_knowledge::unknown;
|
|
/// @brief When true, `incoming_carry_value` is public (window mode).
|
|
bool incoming_carry_public = false;
|
|
/// @brief Public incoming carry when `incoming_carry_public`.
|
|
unsigned incoming_carry_value = 0;
|
|
/// @brief Ask for both carries when the mode would otherwise drop one.
|
|
/// A reduced-ring modulus still kills the sign term.
|
|
bool fuse = false;
|
|
};
|
|
|
|
/// @brief Primitives the evaluator must materialise for one request.
|
|
struct carry_recipe
|
|
{
|
|
carry_mode mode = carry_mode::truncate_reduce;
|
|
unsigned n = 0;
|
|
unsigned s = 0;
|
|
unsigned out_n = 0;
|
|
sign_knowledge sign = sign_knowledge::unknown;
|
|
|
|
/// @brief Low-limb overflow `1{v0+v1 >= 2^s}` (truncate-reduce / fused).
|
|
bool use_low_lt = false;
|
|
/// @brief Comparison of `x` against `2^{n-1}` (secret-sign carry-out).
|
|
bool use_msb_lt = false;
|
|
/// @brief Beaver AND of the two share MSBs (known-sign, or fused with msb).
|
|
bool use_share_msb_and = false;
|
|
/// @brief When true the AND is of the complemented share MSBs (NOR).
|
|
bool and_is_nor = false;
|
|
/// @brief Signed extension: wrap after the public bias `2^{n-1}`.
|
|
bool use_biased_wrap = false;
|
|
/// @brief Window: `1{p0+p1 >= 2^d}`.
|
|
bool use_window_overflow = false;
|
|
/// @brief Window: `1{p0+p1 == 2^d-1}` (only when incoming carry is secret).
|
|
bool use_window_eq = false;
|
|
/// @brief Window: product of equality bit and secret incoming carry.
|
|
bool use_window_product = false;
|
|
|
|
/// @brief Units of `±2^{n-s}` applied after the share-MSB AND.
|
|
std::int64_t and_unit = 0;
|
|
/// @brief Payload of the MSB comparison when it alone selects the unit.
|
|
std::int64_t msb_true_payload = 0;
|
|
/// @brief Payload for the biased wrap (`2^n` in the wide ring).
|
|
std::int64_t wrap_payload = 0;
|
|
/// @brief Window digit width (`s`).
|
|
unsigned window_d = 0;
|
|
/// @brief Public incoming carry absorbed locally when known.
|
|
unsigned public_incoming = 0;
|
|
/// @brief True when the recipe named at least one interactive step.
|
|
bool interactive() const noexcept
|
|
{
|
|
return use_low_lt || use_msb_lt || use_share_msb_and || use_biased_wrap
|
|
|| use_window_overflow || use_window_eq || use_window_product;
|
|
}
|
|
};
|
|
|
|
namespace carry_plan_detail
|
|
{
|
|
|
|
inline std::int64_t unit_pow2(unsigned e)
|
|
{
|
|
if (e >= 63u)
|
|
throw std::invalid_argument("carry_plan: unit does not fit in int64");
|
|
return std::int64_t{1} << e;
|
|
}
|
|
|
|
} // namespace carry_plan_detail
|
|
|
|
/// @brief Select the cheapest live primitives for `req`.
|
|
/// @param req the request
|
|
/// @return a recipe whose flags name only the corrections that survive
|
|
/// @throws std::invalid_argument on inconsistent widths
|
|
/// \complexity A constant number of comparisons of `n`, `s`, `out_n`, and `sign`. No allocation beyond the returned `carry_recipe`. `Θ(1)`.
|
|
/// @see grotto::make_carry_keys
|
|
HEDLEY_WARN_UNUSED_RESULT
|
|
inline carry_recipe plan_carry(const carry_request & req)
|
|
{
|
|
if (req.n == 0u)
|
|
throw std::invalid_argument("carry_plan: n must be positive");
|
|
if (req.out_n == 0u)
|
|
throw std::invalid_argument("carry_plan: out_n must be positive");
|
|
|
|
carry_recipe r{};
|
|
r.mode = req.mode;
|
|
r.n = req.n;
|
|
r.s = req.s;
|
|
r.out_n = req.out_n;
|
|
r.sign = req.sign;
|
|
|
|
const bool reduced_ring = (req.out_n + req.s == req.n) && (req.s > 0u);
|
|
const bool same_width = (req.out_n == req.n);
|
|
|
|
switch (req.mode)
|
|
{
|
|
case carry_mode::truncate_reduce:
|
|
{
|
|
if (req.s == 0u || req.s >= req.n)
|
|
throw std::invalid_argument("carry_plan: truncate_reduce needs 0 < s < n");
|
|
if (req.out_n != req.n - req.s)
|
|
throw std::invalid_argument(
|
|
"carry_plan: truncate_reduce out_n must equal n-s");
|
|
r.use_low_lt = true;
|
|
// Sign wrap is 0 in Z/2^{n-s}; never emit a carry-out term.
|
|
break;
|
|
}
|
|
case carry_mode::same_ring:
|
|
{
|
|
if (req.s >= req.n)
|
|
throw std::invalid_argument("carry_plan: same_ring needs s < n");
|
|
if (!same_width)
|
|
{
|
|
// Caller asked for a fused shift that reduces the ring: drop
|
|
// carry-out and keep only the low-limb correction.
|
|
if (req.out_n != req.n - req.s)
|
|
throw std::invalid_argument(
|
|
"carry_plan: same_ring out_n must be n or n-s");
|
|
r.use_low_lt = true;
|
|
break;
|
|
}
|
|
if (req.fuse || req.s > 0u)
|
|
r.use_low_lt = (req.s > 0u) && req.fuse;
|
|
|
|
if (req.sign == sign_knowledge::unknown)
|
|
{
|
|
r.use_msb_lt = true;
|
|
r.use_share_msb_and = true;
|
|
r.msb_true_payload = 1; // returns msb bit; combined with AND below
|
|
r.and_unit = carry_plan_detail::unit_pow2(req.n - req.s);
|
|
}
|
|
else if (req.sign == sign_knowledge::nonnegative)
|
|
{
|
|
r.use_share_msb_and = true;
|
|
r.and_is_nor = false;
|
|
r.and_unit = carry_plan_detail::unit_pow2(req.n - req.s);
|
|
}
|
|
else
|
|
{
|
|
r.use_share_msb_and = true;
|
|
r.and_is_nor = true;
|
|
r.and_unit = -carry_plan_detail::unit_pow2(req.n - req.s);
|
|
}
|
|
break;
|
|
}
|
|
case carry_mode::extend:
|
|
{
|
|
if (req.out_n <= req.n)
|
|
throw std::invalid_argument("carry_plan: extend needs out_n > n");
|
|
// Known sign does not switch to the share-MSB AND.
|
|
r.use_biased_wrap = true;
|
|
r.wrap_payload = carry_plan_detail::unit_pow2(req.n);
|
|
break;
|
|
}
|
|
case carry_mode::window:
|
|
{
|
|
if (req.s == 0u || req.s > req.n)
|
|
throw std::invalid_argument("carry_plan: window needs 0 < s <= n");
|
|
r.window_d = req.s;
|
|
r.use_window_overflow = true;
|
|
if (req.incoming_carry_public)
|
|
{
|
|
r.public_incoming = req.incoming_carry_value;
|
|
// Public carry 0 drops the equality; a public 1 still needs the
|
|
// all-ones test, but the product is local (multiply by 1).
|
|
if (req.incoming_carry_value != 0u)
|
|
r.use_window_eq = true;
|
|
}
|
|
else
|
|
{
|
|
r.use_window_eq = true;
|
|
r.use_window_product = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fused same-ring request that somehow landed with a reduced modulus:
|
|
// the planner already dropped the sign term above via `!same_width`.
|
|
(void)reduced_ring;
|
|
return r;
|
|
}
|
|
|
|
/// @brief Fill a truncate-and-reduce request and plan it.
|
|
/// \complexity Fills a `carry_request` and calls `plan_carry`. `Θ(1)`.
|
|
/// @see grotto::plan_carry
|
|
HEDLEY_WARN_UNUSED_RESULT
|
|
inline carry_recipe plan_carry_in(unsigned n, unsigned s)
|
|
{
|
|
carry_request req{};
|
|
req.n = n;
|
|
req.s = s;
|
|
req.out_n = n - s;
|
|
req.mode = carry_mode::truncate_reduce;
|
|
return plan_carry(req);
|
|
}
|
|
|
|
/// @brief Fill a same-ring carry-out request and plan it.
|
|
/// \complexity Fills a `carry_request` and calls `plan_carry`. `Θ(1)`.
|
|
/// @see grotto::plan_carry
|
|
HEDLEY_WARN_UNUSED_RESULT
|
|
inline carry_recipe plan_carry_out(unsigned n, unsigned s, sign_knowledge sign)
|
|
{
|
|
carry_request req{};
|
|
req.n = n;
|
|
req.s = s;
|
|
req.out_n = n;
|
|
req.mode = carry_mode::same_ring;
|
|
req.sign = sign;
|
|
req.fuse = false;
|
|
return plan_carry(req);
|
|
}
|
|
|
|
/// @brief Fill a fused same-ring exact-shift request and plan it.
|
|
/// @details When `out_n == n-s` the sign term is dropped automatically.
|
|
/// \complexity Fills a `carry_request` and calls `plan_carry`. `Θ(1)`.
|
|
/// @see grotto::plan_carry
|
|
HEDLEY_WARN_UNUSED_RESULT
|
|
inline carry_recipe plan_carry_fused(unsigned n, unsigned s, unsigned out_n,
|
|
sign_knowledge sign = sign_knowledge::unknown)
|
|
{
|
|
carry_request req{};
|
|
req.n = n;
|
|
req.s = s;
|
|
req.out_n = out_n;
|
|
req.mode = carry_mode::same_ring;
|
|
req.sign = sign;
|
|
req.fuse = true;
|
|
return plan_carry(req);
|
|
}
|
|
|
|
} // namespace grotto
|
|
|
|
#endif // LIBDPF_INCLUDE_GROTTO_CARRY_PLAN_HPP__
|