libdpf/include/dpf/net/trio.hpp
Ryan Henry 0d22946a0e 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>
2026-09-28 05:59:19 -06:00

563 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// @file dpf/net/trio.hpp
/// @brief Dial/accept three-party (2+1) socket mesh for libdpf protocols.
/// @details Socket paths live under a driver-created directory:
/// `p0-p1`, `p0-p2`, `p1-p2`. The lower role accepts; the higher
/// dials. Helpers `deal` / `accept_deal` / `open_with` name the
/// protocol steps without exposing ASIO.
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
/// see [LICENSE.md](@ref license) for details.
#ifndef LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
#define LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
#include <unistd.h>
#include "dpf/net/asio_ns.hpp"
#include "dpf/net/channel.hpp"
#include "dpf/net/comm_hook.hpp"
#include "dpf/net/policy.hpp"
#include "dpf/net/round_sink.hpp"
#include "dpf/net/secure_channel.hpp"
#include "dpf/net/security.hpp"
#include "dpf/net/socket_tune.hpp"
#include "hedley/hedley.h"
namespace dpf
{
namespace net
{
/// @brief Party roles in a (2+1) run. p2 is the dealer.
enum class role : unsigned
{
p0 = 0,
p1 = 1,
p2 = 2,
};
HEDLEY_CONST
HEDLEY_NO_THROW
inline constexpr unsigned to_u(role r) noexcept
{
return static_cast<unsigned>(r);
}
HEDLEY_CONST
HEDLEY_NO_THROW
inline const char * role_name(role r) noexcept
{
switch (r)
{
case role::p0: return "p0";
case role::p1: return "p1";
case role::p2: return "p2";
}
return "?";
}
inline std::string link_path(const std::string & dir, role a, role b)
{
if (to_u(a) > to_u(b))
std::swap(a, b);
return dir + "/" + role_name(a) + "-" + role_name(b);
}
/// @brief Connected view of the other two parties.
class trio
{
public:
trio() = default;
/// @brief Connect local unix-domain sockets under `dir`.
/// @param self this process's role
/// @param dir directory holding the three socket paths
/// @param retries dial/accept attempts before giving up
/// @param sec same peer TLS policy as `party_session` (default encrypt)
static trio connect_local(role self, const std::string & dir,
unsigned retries = 200, const peer_security & sec = {},
const deadlines & lim = {})
{
trio t;
t.self_ = self;
t.io_ = std::make_unique<asio::io_context>();
for (unsigned other = 0; other < 3; ++other)
{
if (other == to_u(self))
continue;
auto peer = static_cast<role>(other);
t.link_[other] = connect_one(self, peer, dir, *t.io_, retries, sec,
lim);
}
return t;
}
/// @brief Connect the p0–p1 socket only. The dealer link stays closed
/// until `install_inbox`.
static trio connect_pair(role self, const std::string & dir,
unsigned retries = 200, const peer_security & sec = {},
const deadlines & lim = {})
{
if (self == role::p2)
throw std::invalid_argument("connect_pair is p0 and p1");
trio t;
t.self_ = self;
t.io_ = std::make_unique<asio::io_context>();
const role peer = self == role::p0 ? role::p1 : role::p0;
t.link_[to_u(peer)] = connect_one(self, peer, dir, *t.io_, retries, sec,
lim);
return t;
}
/// @brief Serve later `to(p2).recv` calls from a local frame buffer.
void install_inbox(std::vector<std::uint8_t> frames)
{
if (self_ == role::p2)
throw std::logic_error("p2 has no dealer inbox");
link_[to_u(role::p2)] = channel::from_inbox(std::move(frames));
}
/// @brief True when the installed dealer tape has been fully consumed.
HEDLEY_NO_THROW
bool dealer_inbox_done() const noexcept
{
if (self_ == role::p2)
return false;
return link_[to_u(role::p2)].inbox_done();
}
/// @brief Connect over TCP loopback. Ports are `base + 10*lo + hi`.
static trio connect_tcp(role self, std::uint16_t base_port,
const std::string & host = "127.0.0.1", unsigned retries = 200,
const peer_security & sec = {}, const deadlines & lim = {},
const socket_options & so = {})
{
trio t;
t.self_ = self;
t.io_ = std::make_unique<asio::io_context>();
for (unsigned other = 0; other < 3; ++other)
{
if (other == to_u(self))
continue;
auto peer = static_cast<role>(other);
t.link_[other] = connect_one_tcp(self, peer, base_port, host,
*t.io_, retries, sec, lim, so);
}
return t;
}
role self() const noexcept { return self_; }
/// @brief Sum of byte and frame counters on the open links.
HEDLEY_NO_THROW
io_tally tally() const noexcept
{
io_tally sum;
for (const auto & link : link_)
{
if (link.open())
sum += link.tally();
}
return sum;
}
/// @brief Counters on the link to `peer` only.
HEDLEY_NO_THROW
io_tally tally_to(role peer) const noexcept
{
if (peer == self_ || !link_[to_u(peer)].open())
return {};
return link_[to_u(peer)].tally();
}
/// @brief Bytes received from the dealer (p2). Empty for the dealer itself.
HEDLEY_NO_THROW
io_tally tally_from_p2() const noexcept
{
if (self_ == role::p2)
return {};
return tally_to(role::p2);
}
/// @brief Bytes with the other computing party (p0↔p1).
/// @details For p2 this is the sum of the links to p0 and p1.
HEDLEY_NO_THROW
io_tally tally_from_peer() const noexcept
{
if (self_ == role::p0)
return tally_to(role::p1);
if (self_ == role::p1)
return tally_to(role::p0);
io_tally sum;
sum += tally_to(role::p0);
sum += tally_to(role::p1);
return sum;
}
/// @brief Zero counters on every open link.
HEDLEY_NO_THROW
void reset_tally() noexcept
{
for (auto & link : link_)
{
if (link.open())
link.reset_tally();
}
}
/// @brief The open channel to `peer` (raw bypass; does not use the hook).
/// @param peer the other party
/// @return that channel
/// @throws std::invalid_argument if `peer` is this process
/// @throws std::logic_error if that link was not connected
channel & to(role peer)
{
if (peer == self_)
throw std::invalid_argument("trio::to(self)");
auto & c = link_[to_u(peer)];
if (!c.open())
throw std::logic_error("trio link is not open");
return c;
}
/// @brief Install a replaceable transport for helpers and `batch`.
/// @details Non-owning. Null restores the framed mesh defaults.
void set_hook(comm_hook * hook) noexcept { hook_ = hook; }
/// @brief Current hook, or null when helpers use the mesh directly.
HEDLEY_NO_THROW
comm_hook * hook() const noexcept { return hook_; }
/// @brief One-way send through the hook (or the mesh when unset).
template <typename T>
void send_to(role peer, msg tag, const T & value)
{
static_assert(std::is_trivially_copyable_v<T>,
"trio::send_to requires a trivially copyable type");
if (hook_)
{
hook_->send_bytes(*this, to_u(peer), tag, &value, sizeof(T));
return;
}
to(peer).send(tag, value);
}
/// @brief Homogeneous vector send through the hook (or the mesh).
template <typename T>
void send_vec_to(role peer, msg tag, const std::vector<T> & values)
{
static_assert(std::is_trivially_copyable_v<T>,
"trio::send_vec_to requires a trivially copyable type");
if (hook_)
{
const std::size_t nbytes = values.size() * sizeof(T);
hook_->send_bytes(*this, to_u(peer), tag, values.data(), nbytes);
return;
}
to(peer).send_vec(values, tag);
}
/// @brief Untyped one-way send through the hook (or the mesh).
void send_bytes_to(role peer, msg tag, const void * data, std::size_t n)
{
if (hook_)
{
hook_->send_bytes(*this, to_u(peer), tag, data, n);
return;
}
to(peer).send_bytes(tag, data, n);
}
/// @brief Untyped one-way receive through the hook (or the mesh).
HEDLEY_WARN_UNUSED_RESULT
std::vector<std::uint8_t> recv_bytes_from(role peer, msg tag)
{
if (hook_)
return hook_->recv_bytes(*this, to_u(peer), tag);
return to(peer).recv_bytes(tag);
}
/// @brief One-way receive through the hook (or the mesh when unset).
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
T recv_from(role peer, msg tag)
{
static_assert(std::is_trivially_copyable_v<T>,
"trio::recv_from requires a trivially copyable type");
if (hook_)
{
auto bytes = hook_->recv_bytes(*this, to_u(peer), tag);
if (bytes.size() != sizeof(T))
throw std::runtime_error("trio::recv_from size mismatch");
T value{};
if constexpr (sizeof(T) != 0)
std::memcpy(&value, bytes.data(), sizeof(T));
return value;
}
return to(peer).template recv<T>(tag);
}
/// @brief Homogeneous vector receive through the hook (or the mesh).
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> recv_vec_from(role peer, msg tag = msg::beaver_tape)
{
static_assert(std::is_trivially_copyable_v<T>,
"trio::recv_vec_from requires a trivially copyable type");
if (hook_)
{
auto bytes = hook_->recv_bytes(*this, to_u(peer), tag);
if constexpr (sizeof(T) == 0)
{
if (!bytes.empty())
throw std::runtime_error("trio::recv_vec_from size");
return {};
}
if (bytes.size() % sizeof(T) != 0)
throw std::runtime_error("trio::recv_vec_from size mismatch");
std::vector<T> out(bytes.size() / sizeof(T));
if (!out.empty())
std::memcpy(out.data(), bytes.data(), bytes.size());
return out;
}
return to(peer).template recv_vec<T>(tag);
}
/// @brief Round-batched peer sink. Default is a mux on the p0–p1 link.
/// @details Defined next to `mux_sink` so the default factory can build one.
std::unique_ptr<RoundSink> batch(std::size_t count,
std::vector<std::size_t> slot_bytes);
/// @brief Dealer sends one side of a split to each computing party.
/// @tparam Split a type with `p0` and `p1` members
/// @param s the split
/// @throws std::logic_error if this process is not p2
template <typename Split>
void deal(const Split & s)
{
if (self_ != role::p2)
throw std::logic_error("only the dealer may deal");
send_to(role::p0, msg::beaver_tape, s.p0);
send_to(role::p1, msg::beaver_tape, s.p1);
}
/// @brief Computing party receives its share of a dealt value.
/// @tparam Ring the dealt type
/// @return this party's share
/// @throws std::logic_error if this process is p2
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
Ring accept_deal()
{
if (self_ == role::p2)
throw std::logic_error("dealer does not accept_deal");
return recv_from<Ring>(role::p2, msg::beaver_tape);
}
/// @brief Open an additive share with the peer computing party.
/// @tparam Ring additive ring
/// @param peer the other computing party
/// @param mine this party's share
/// @return reconstructed public value (`mine + peer`)
/// @throws std::logic_error if either side is p2
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
Ring open_with(role peer, const Ring & mine)
{
if (self_ == role::p2 || peer == role::p2)
throw std::logic_error("open_with is between p0 and p1");
Ring theirs = exchange_with(peer, mine, msg::delta);
return static_cast<Ring>(mine + theirs);
}
/// @brief Exchange raw values (no algebra) with a peer.
/// @tparam T trivially copyable payload
/// @param peer the other party
/// @param mine this party's value
/// @param tag the message tag
/// @return the peer's value
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
T exchange_with(role peer, const T & mine, msg tag = msg::delta)
{
static_assert(std::is_trivially_copyable_v<T>,
"trio::exchange_with requires a trivially copyable type");
if (hook_)
{
auto bytes = hook_->exchange_bytes(*this, to_u(peer), tag, &mine,
sizeof(T));
if (bytes.size() != sizeof(T))
throw std::runtime_error("trio::exchange_with size mismatch");
T theirs{};
if constexpr (sizeof(T) != 0)
std::memcpy(&theirs, bytes.data(), sizeof(T));
return theirs;
}
return to(peer).template exchange<T>(to_u(self_), to_u(peer), mine, tag);
}
/// @brief Exchange a homogeneous vector with a peer (one barrier).
/// @tparam T trivially copyable element
/// @param peer the other party
/// @param mine this party's values
/// @param tag the message tag
/// @return the peer's values
template <typename T>
HEDLEY_WARN_UNUSED_RESULT
std::vector<T> exchange_vec_with(role peer, const std::vector<T> & mine,
msg tag = msg::ring_vector)
{
static_assert(std::is_trivially_copyable_v<T>,
"trio::exchange_vec_with requires a trivially copyable type");
if (mine.empty())
return {};
if (hook_)
{
const std::size_t nbytes = mine.size() * sizeof(T);
auto bytes = hook_->exchange_vec_bytes(*this, to_u(peer), tag,
mine.data(), nbytes);
if (bytes.size() != nbytes)
throw std::runtime_error("trio::exchange_vec_with size mismatch");
std::vector<T> theirs(mine.size());
std::memcpy(theirs.data(), bytes.data(), nbytes);
return theirs;
}
return to(peer).template exchange_vec<T>(
to_u(self_), to_u(peer), mine, tag);
}
/// @brief Open additive shares packed into one vector exchange.
/// @tparam Ring additive ring
/// @param peer the other computing party
/// @param mine this party's shares
/// @return reconstructed public values (`mine[i] + peer[i]`)
template <typename Ring>
HEDLEY_WARN_UNUSED_RESULT
std::vector<Ring> open_vec_with(role peer, const std::vector<Ring> & mine)
{
if (self_ == role::p2 || peer == role::p2)
throw std::logic_error("open_vec_with is between p0 and p1");
auto theirs = exchange_vec_with(peer, mine);
std::vector<Ring> out(mine.size());
for (std::size_t i = 0; i < mine.size(); ++i)
out[i] = static_cast<Ring>(mine[i] + theirs[i]);
return out;
}
private:
role self_ = role::p0;
std::unique_ptr<asio::io_context> io_;
channel link_[3];
comm_hook * hook_ = nullptr;
static channel connect_one(role self, role peer, const std::string & dir,
asio::io_context & io, unsigned retries, const peer_security & sec,
const deadlines & lim)
{
using proto = asio::local::stream_protocol;
const bool accept = to_u(self) < to_u(peer);
const auto path = link_path(dir, self, peer);
const std::string who = role_name(peer);
if (accept)
{
::unlink(path.c_str());
proto::endpoint ep(path);
proto::acceptor acc(io, ep);
for (unsigned i = 0; i < retries; ++i)
{
asio::error_code ec;
proto::socket sock(io);
acc.accept(sock, ec);
if (!ec)
return secure_local_channel(io, std::move(sock), true,
to_u(peer), sec, lim.handshake, who, "accept");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
throw std::runtime_error("trio accept failed: " + path);
}
proto::endpoint ep(path);
for (unsigned i = 0; i < retries; ++i)
{
asio::error_code ec;
proto::socket sock(io);
sock.connect(ep, ec);
if (!ec)
return secure_local_channel(io, std::move(sock), false,
to_u(peer), sec, lim.handshake, who, "connect");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
throw std::runtime_error("trio connect failed: " + path);
}
static std::uint16_t tcp_port(std::uint16_t base, role a, role b)
{
if (to_u(a) > to_u(b))
std::swap(a, b);
return static_cast<std::uint16_t>(base + 10u * to_u(a) + to_u(b));
}
static channel connect_one_tcp(role self, role peer, std::uint16_t base,
const std::string & host, asio::io_context & io, unsigned retries,
const peer_security & sec, const deadlines & lim,
const socket_options & so)
{
using tcp = asio::ip::tcp;
const bool accept = to_u(self) < to_u(peer);
const auto port = tcp_port(base, self, peer);
const std::string who = role_name(peer);
if (accept)
{
tcp::endpoint ep(tcp::v4(), port);
tcp::acceptor acc(io);
asio::error_code ec;
acc.open(ep.protocol(), ec);
acc.set_option(tcp::acceptor::reuse_address(true), ec);
acc.bind(ep, ec);
if (ec)
throw std::runtime_error("trio tcp bind: " + ec.message());
acc.listen(1, ec);
for (unsigned i = 0; i < retries; ++i)
{
tcp::socket sock(io);
acc.accept(sock, ec);
if (!ec)
return secure_tcp_channel(io, std::move(sock), true,
to_u(peer), sec, so, lim.handshake, who, "accept");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
throw std::runtime_error("trio tcp accept failed");
}
tcp::resolver resolver(io);
auto endpoints = resolver.resolve(host, std::to_string(port));
for (unsigned i = 0; i < retries; ++i)
{
asio::error_code ec;
tcp::socket sock(io);
asio::connect(sock, endpoints, ec);
if (!ec)
return secure_tcp_channel(io, std::move(sock), false,
to_u(peer), sec, so, lim.handshake, who, "connect");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
throw std::runtime_error("trio tcp connect failed");
}
};
} // namespace net
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__