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

443
include/dpf/shuffle.hpp Normal file
View file

@ -0,0 +1,443 @@
/// @file dpf/shuffle.hpp
/// @brief Honest-majority 3PC shuffle and boolean select / aggregates.
/// @details `shuffle_party` applies one permutation known to every party.
/// `shuffle_hidden_pass` is the hidden order: three passes, each
/// from one pairwise seed, leaving that seed's missing party out.
/// Passes run left-out 2 (`k01`), then 0 (`k12`), then 1 (`k20`).
/// `permute` is `out[i] = in[pi[i]]`, and each pass applies that map,
/// so the clear replay is `permute(permute(permute(v, π01), π12), π20)`.
/// A secret index stays a DPF. This header reorders a column of
/// shares the parties already hold. The manual is
/// [An array of shares, not a secret index](@ref share_shuffle).
#ifndef LIBDPF_INCLUDE_DPF_SHUFFLE_HPP__
#define LIBDPF_INCLUDE_DPF_SHUFFLE_HPP__
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <numeric>
#include <stdexcept>
#include <utility>
#include <vector>
#include "hedley/hedley.h"
#include "dpf/bit_inject.hpp"
#include "dpf/buffered_prg.hpp"
#include "dpf/prg_aes.hpp"
#include "dpf/random.hpp"
#include "dpf/rss_seed.hpp"
#include "dpf/share_cmp.hpp"
namespace dpf
{
namespace shuffle
{
/// @brief Fisher–Yates permutation from one pairwise seed. Lane 0.
HEDLEY_WARN_UNUSED_RESULT
inline std::vector<std::size_t> permutation_from_seed(
rss::seed_block seed, std::size_t n, std::uint64_t index)
{
std::vector<std::size_t> pi(n);
std::iota(pi.begin(), pi.end(), 0);
randomness::lane_table<std::uint64_t, prg::aes128> table(seed);
for (std::size_t i = n; i > 1; --i)
{
const auto r = table.value_at(0, index + i);
const std::size_t j = static_cast<std::size_t>(r % i);
std::swap(pi[i - 1], pi[j]);
}
return pi;
}
/// @brief Derive a shared permutation of `n` from pairwise seeds (dealer view).
HEDLEY_WARN_UNUSED_RESULT
inline std::vector<std::size_t> permutation_from_seeds(
const rss::seed_bundle & bundle, std::size_t n, std::uint64_t index)
{
return permutation_from_seed(bundle.k01, n, index);
}
/// @brief Apply permutation to a clear vector.
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> permute(const std::vector<T> & v,
const std::vector<std::size_t> & pi)
{
if (v.size() != pi.size())
throw std::invalid_argument("shuffle permute");
std::vector<T> out(v.size());
for (std::size_t i = 0; i < pi.size(); ++i)
out[i] = v[pi[i]];
return out;
}
/// @brief Multiset oracle: permute a clear vector (not a party protocol).
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> shuffle_clear(const std::vector<T> & values,
const rss::seed_bundle & bundle, std::uint64_t index)
{
auto pi = permutation_from_seeds(bundle, values.size(), index);
return permute(values, pi);
}
/// @brief One 3PC party's shuffle of RSS components.
/// @details Party holds replicated vectors `(own[i], next[i])`. Permute both
/// by the shared `pi`, then the caller `send_next`s the permuted `own`
/// and refreshes with the received previous component.
template <typename T>
struct shuffle_party_view
{
std::vector<T> own;
std::vector<T> next;
};
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
shuffle_party_view<T> shuffle_party(const shuffle_party_view<T> & in,
const rss::seed_bundle & bundle, std::uint64_t index)
{
auto pi = permutation_from_seeds(bundle, in.own.size(), index);
if (in.next.size() != in.own.size())
throw std::invalid_argument("shuffle_party size");
shuffle_party_view<T> out;
out.own = permute(in.own, pi);
out.next = permute(in.next, pi);
return out;
}
/// @brief Party `me` sends to `(me + 1) mod 3` on a hidden pass, or to nobody.
inline unsigned hidden_next(unsigned me) noexcept
{
return (me + 1u) % 3u;
}
/// @brief The party who forms `u = own + next` on the pass that leaves `left_out` out.
inline unsigned hidden_u_party(unsigned left_out) noexcept
{
return (left_out + 1u) % 3u;
}
/// @brief The party who permutes its `next` component on that pass.
inline unsigned hidden_side_party(unsigned left_out) noexcept
{
return (left_out + 2u) % 3u;
}
/// @brief One directed array from `shuffle_hidden_pass`.
template <typename T>
struct hidden_message
{
std::vector<T> data;
unsigned to = 0;
bool sends = false;
};
/// @brief Local view after one pass, plus the array this party sends to `next`.
template <typename T>
struct hidden_pass_result
{
shuffle_party_view<T> view;
hidden_message<T> out;
};
namespace detail
{
inline rss::seed_block seed_toward(const rss::party_seeds & seeds, unsigned other)
{
if (other == hidden_next(seeds.me))
return seeds.with_next;
if (other == hidden_next(hidden_next(seeds.me)))
return seeds.with_prev;
throw std::invalid_argument("shuffle: seed");
}
template <typename T>
std::vector<T> words_from_seed(rss::seed_block seed, std::uint32_t lane,
std::size_t n, std::uint64_t index)
{
randomness::lane_table<std::uint64_t, prg::aes128> table(seed);
std::vector<T> out(n);
for (std::size_t i = 0; i < n; ++i)
out[i] = static_cast<T>(table.value_at(lane, index + i));
return out;
}
} // namespace detail
/// @brief One hidden-shuffle pass on replicated `(own, next)`.
/// @details Passes run with `left_out` 2, then 0, then 1. Parties who share
/// the pass seed permute a two-party split; the left-out party
/// receives a fresh component. `inbound` is required for the side
/// party and the left-out party (the array their predecessor sends).
/// The u-party ignores `inbound`. Pad words use lane 1 of the
/// permutation seed. The fresh mask uses lane 2 of the seed shared
/// with the left-out party, so it does not collide with that seed's
/// permutation lane.
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
hidden_pass_result<T> shuffle_hidden_pass(unsigned me,
const rss::party_seeds & seeds, const shuffle_party_view<T> & in,
std::uint64_t index, unsigned left_out, const std::vector<T> * inbound)
{
if (me > 2 || left_out > 2)
throw std::invalid_argument("shuffle: party");
if (seeds.me != me)
throw std::invalid_argument("shuffle: party seeds");
if (in.own.size() != in.next.size())
throw std::invalid_argument("shuffle: size");
const std::size_t n = in.own.size();
const unsigned u = hidden_u_party(left_out);
const unsigned side = hidden_side_party(left_out);
hidden_pass_result<T> step;
if (me == u)
{
const auto pi_seed = detail::seed_toward(seeds, side);
const auto mask_seed = detail::seed_toward(seeds, left_out);
const auto pi = permutation_from_seed(pi_seed, n, index);
const auto pad = detail::words_from_seed<T>(pi_seed, 1, n, index);
const auto mask = detail::words_from_seed<T>(mask_seed, 2, n, index);
std::vector<T> sum(n);
for (std::size_t i = 0; i < n; ++i)
sum[i] = static_cast<T>(in.own[i] + in.next[i] + pad[i]);
const auto moved = permute(sum, pi);
step.view.own = mask;
step.view.next.resize(n);
step.out.data.resize(n);
for (std::size_t i = 0; i < n; ++i)
{
step.view.next[i] = static_cast<T>(moved[i] - mask[i]);
step.out.data[i] = step.view.next[i];
}
step.out.to = side;
step.out.sends = true;
return step;
}
if (me == side)
{
if (inbound == nullptr || inbound->size() != n)
throw std::invalid_argument("shuffle: inbound");
const auto pi_seed = detail::seed_toward(seeds, u);
const auto pi = permutation_from_seed(pi_seed, n, index);
const auto pad = detail::words_from_seed<T>(pi_seed, 1, n, index);
std::vector<T> side_v(n);
for (std::size_t i = 0; i < n; ++i)
side_v[i] = static_cast<T>(in.next[i] - pad[i]);
step.view.next = permute(side_v, pi);
step.view.own = *inbound;
step.out.data = step.view.next;
step.out.to = left_out;
step.out.sends = true;
return step;
}
if (inbound == nullptr || inbound->size() != n)
throw std::invalid_argument("shuffle: inbound");
const auto mask_seed = detail::seed_toward(seeds, u);
step.view.next = detail::words_from_seed<T>(mask_seed, 2, n, index);
step.view.own = *inbound;
step.out.sends = false;
return step;
}
/// @brief Three hidden passes. Opens by summing the three `own` components.
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> shuffle_hidden_triple(const std::vector<T> & clear,
const rss::seed_bundle & bundle, std::uint64_t index)
{
const std::size_t n = clear.size();
shuffle_party_view<T> held[3];
for (unsigned p = 0; p < 3; ++p)
{
held[p].own.assign(n, T{});
held[p].next.assign(n, T{});
}
for (std::size_t i = 0; i < n; ++i)
{
const T a = dpf::uniform_sample<T>();
const T b = dpf::uniform_sample<T>();
const T c = static_cast<T>(clear[i] - a - b);
held[0].own[i] = a;
held[0].next[i] = b;
held[1].own[i] = b;
held[1].next[i] = c;
held[2].own[i] = c;
held[2].next[i] = a;
}
const unsigned order[3] = {2u, 0u, 1u};
for (unsigned left : order)
{
const unsigned u = hidden_u_party(left);
const unsigned side = hidden_side_party(left);
const auto seeds_u = rss::party_seeds::from_bundle(bundle, u);
const auto seeds_s = rss::party_seeds::from_bundle(bundle, side);
const auto seeds_l = rss::party_seeds::from_bundle(bundle, left);
auto u_step = shuffle_hidden_pass<T>(u, seeds_u, held[u], index, left, nullptr);
auto s_step = shuffle_hidden_pass<T>(
side, seeds_s, held[side], index, left, &u_step.out.data);
auto l_step = shuffle_hidden_pass<T>(
left, seeds_l, held[left], index, left, &s_step.out.data);
held[u] = std::move(u_step.view);
held[side] = std::move(s_step.view);
held[left] = std::move(l_step.view);
}
std::vector<T> out(n);
for (std::size_t i = 0; i < n; ++i)
out[i] = static_cast<T>(held[0].own[i] + held[1].own[i] + held[2].own[i]);
return out;
}
/// @brief After ring send of `own`: party stores `(own, recv_from_prev)`.
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
shuffle_party_view<T> shuffle_refresh(std::vector<T> own,
std::vector<T> from_prev)
{
if (own.size() != from_prev.size())
throw std::invalid_argument("shuffle_refresh size");
return shuffle_party_view<T>{std::move(own), std::move(from_prev)};
}
/// @brief Dealer helper: run three parties' permute + ring refresh; open sum.
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> shuffle_party_triple(const std::vector<T> & clear,
const rss::seed_bundle & bundle, std::uint64_t index)
{
// Share as RSS: p0=(v,0), p1=(0,0), p2=(0,v) component-wise is wrong for
// sum; use additive split into three: sample r0,r1, r2=v-r0-r1.
const std::size_t n = clear.size();
shuffle_party_view<T> v0{std::vector<T>(n), std::vector<T>(n)};
shuffle_party_view<T> v1{std::vector<T>(n), std::vector<T>(n)};
shuffle_party_view<T> v2{std::vector<T>(n), std::vector<T>(n)};
for (std::size_t i = 0; i < n; ++i)
{
const T a = dpf::uniform_sample<T>();
const T b = dpf::uniform_sample<T>();
const T c = static_cast<T>(clear[i] - a - b);
// p0:(a,b) p1:(b,c) p2:(c,a)
v0.own[i] = a;
v0.next[i] = b;
v1.own[i] = b;
v1.next[i] = c;
v2.own[i] = c;
v2.next[i] = a;
}
auto p0 = shuffle_party(v0, bundle, index);
auto p1 = shuffle_party(v1, bundle, index);
auto p2 = shuffle_party(v2, bundle, index);
// Ring send own → next party; refresh: (own, from_prev) with from_prev =
// previous party's own.
auto r0 = shuffle_refresh(p0.own, p2.own);
auto r1 = shuffle_refresh(p1.own, p0.own);
auto r2 = shuffle_refresh(p2.own, p1.own);
std::vector<T> out(n);
for (std::size_t i = 0; i < n; ++i)
out[i] = static_cast<T>(r0.own[i] + r1.own[i] + r2.own[i]);
return out;
}
/// @brief Multiset equality (permutation check).
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
bool is_permutation_of(std::vector<T> a, std::vector<T> b)
{
if (a.size() != b.size())
return false;
std::sort(a.begin(), a.end());
std::sort(b.begin(), b.end());
return a == b;
}
/// @brief select(pred, row): inject boolean mask onto each column (clear).
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
std::vector<Ring> select_clear(const std::vector<std::uint8_t> & pred,
const std::vector<Ring> & row)
{
if (pred.size() != row.size())
throw std::invalid_argument("select size");
std::vector<Ring> out(row.size());
for (std::size_t i = 0; i < row.size(); ++i)
out[i] = pred[i] ? row[i] : Ring{};
return out;
}
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
Ring sum_if_clear(const std::vector<std::uint8_t> & pred,
const std::vector<Ring> & row)
{
Ring s{};
auto selected = select_clear(pred, row);
for (auto v : selected)
s = static_cast<Ring>(s + v);
return s;
}
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
Ring count_if_clear(const std::vector<std::uint8_t> & pred)
{
Ring c{};
for (auto b : pred)
c = static_cast<Ring>(c + static_cast<Ring>(b & 1u));
return c;
}
/// @brief Argmax over a clear vector: value and one-hot index.
template <typename Ring>
struct argmax_result
{
Ring value{};
std::vector<std::uint8_t> one_hot;
};
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
argmax_result<Ring> argmax_clear(const std::vector<Ring> & v, unsigned nbits)
{
if (v.empty())
throw std::invalid_argument("argmax empty");
argmax_result<Ring> out;
out.one_hot.assign(v.size(), 0);
std::size_t best = 0;
for (std::size_t i = 1; i < v.size(); ++i)
if (share_cmp::gt_clear(v[i], v[best], nbits))
best = i;
out.value = v[best];
out.one_hot[best] = 1;
return out;
}
/// @brief Short 2PC Waksman-style shuffle via mux tree (clear oracle).
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> waksman_clear(const std::vector<T> & v,
const std::vector<std::uint8_t> & swap_bits)
{
auto out = v;
std::size_t bit = 0;
for (std::size_t len = 2; len <= out.size(); len *= 2)
{
for (std::size_t i = 0; i + len / 2 < out.size(); i += len)
{
for (std::size_t j = 0; j < len / 2 && i + j + len / 2 < out.size();
++j)
{
if (bit < swap_bits.size() && swap_bits[bit])
std::swap(out[i + j], out[i + j + len / 2]);
++bit;
}
}
}
return out;
}
} // namespace shuffle
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_SHUFFLE_HPP__