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

@ -0,0 +1,572 @@
/// @file dpf/net/stream_array.hpp
/// @brief Indexed pack of duplex byte streams for dealer and online rounds.
/// @details `read` / `write` / `flush` are the only operations. Backends are
/// memory (tests), file (stored prep), and one-channel mux (TCP).
/// SCTP is a later backend: index `i` maps to association stream `i`.
/// Protocol code must not call TCP or SCTP sockets directly.
#ifndef LIBDPF_INCLUDE_DPF_NET_STREAM_ARRAY_HPP__
#define LIBDPF_INCLUDE_DPF_NET_STREAM_ARRAY_HPP__
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>
#include <vector>
#include <poll.h>
#include <sys/socket.h>
#include "hedley/hedley.h"
#include "dpf/net/channel.hpp"
#include "dpf/net/policy.hpp"
#include "dpf/net/round_lane.hpp"
#include "dpf/net/round_sink.hpp"
namespace dpf
{
namespace net
{
/// @brief Virtual base for N duplex streams.
class stream_array
{
public:
virtual ~stream_array() = default;
virtual std::size_t size() const noexcept = 0;
virtual void write(std::size_t i, const void * src, std::size_t n) = 0;
virtual void read(std::size_t i, void * dst, std::size_t n) = 0;
virtual void flush(std::size_t i) = 0;
void write_all(std::size_t i, const void * src, std::size_t n)
{
write(i, src, n);
flush(i);
}
};
// ---------------------------------------------------------------------------
// Memory
// ---------------------------------------------------------------------------
struct memory_stream_hub
{
std::size_t n = 0;
std::vector<std::vector<std::uint8_t>> a_to_b;
std::vector<std::vector<std::uint8_t>> b_to_a;
std::vector<std::size_t> a_pos;
std::vector<std::size_t> b_pos;
mutable std::mutex mu;
std::condition_variable cv;
explicit memory_stream_hub(std::size_t streams)
: n(streams),
a_to_b(streams),
b_to_a(streams),
a_pos(streams, 0),
b_pos(streams, 0)
{
}
};
/// @brief One end of a paired in-process stream array.
class memory_stream_array : public stream_array
{
public:
memory_stream_array(std::shared_ptr<memory_stream_hub> hub, bool side_a)
: hub_(std::move(hub)), side_a_(side_a)
{
if (!hub_)
throw std::invalid_argument("memory_stream_array needs a hub");
}
std::size_t size() const noexcept override { return hub_->n; }
void write(std::size_t i, const void * src, std::size_t n) override
{
check(i);
{
std::lock_guard<std::mutex> lock(hub_->mu);
auto & q = side_a_ ? hub_->a_to_b[i] : hub_->b_to_a[i];
const auto * p = static_cast<const std::uint8_t *>(src);
q.insert(q.end(), p, p + n);
}
hub_->cv.notify_all();
}
void read(std::size_t i, void * dst, std::size_t n) override
{
check(i);
std::unique_lock<std::mutex> lock(hub_->mu);
auto & q = side_a_ ? hub_->b_to_a[i] : hub_->a_to_b[i];
auto & pos = side_a_ ? hub_->b_pos[i] : hub_->a_pos[i];
hub_->cv.wait(lock, [&] { return pos + n <= q.size(); });
std::memcpy(dst, q.data() + pos, n);
pos += n;
}
void flush(std::size_t) override {}
private:
void check(std::size_t i) const
{
if (i >= hub_->n)
throw std::out_of_range("memory_stream_array index");
}
std::shared_ptr<memory_stream_hub> hub_;
bool side_a_ = true;
};
HEDLEY_WARN_UNUSED_RESULT
inline std::pair<memory_stream_array, memory_stream_array>
make_memory_stream_pair(std::size_t streams)
{
auto hub = std::make_shared<memory_stream_hub>(streams);
return {memory_stream_array(hub, true), memory_stream_array(hub, false)};
}
// ---------------------------------------------------------------------------
// File (one file per stream index)
// ---------------------------------------------------------------------------
/// @brief Read-only or write-only file per stream. Dealer writes; online reads.
class file_stream_array : public stream_array
{
public:
/// @brief Open `basename + "-" + i` for each index.
/// @param write_mode true for dealer output, false for online input.
file_stream_array(const std::string & basename, std::size_t streams,
bool write_mode)
: write_mode_(write_mode), files_(streams)
{
for (std::size_t i = 0; i < streams; ++i)
{
const auto path = basename + "-" + std::to_string(i);
files_[i].open(path,
write_mode ? (std::ios::binary | std::ios::trunc | std::ios::out)
: (std::ios::binary | std::ios::in));
if (!files_[i])
throw std::runtime_error("file_stream_array: " + path);
}
}
std::size_t size() const noexcept override { return files_.size(); }
void write(std::size_t i, const void * src, std::size_t n) override
{
if (!write_mode_)
throw std::logic_error("file_stream_array: write on read-only");
check(i);
files_[i].write(static_cast<const char *>(src),
static_cast<std::streamsize>(n));
if (!files_[i])
throw std::runtime_error("file_stream_array: write failed");
}
void read(std::size_t i, void * dst, std::size_t n) override
{
if (write_mode_)
throw std::logic_error("file_stream_array: read on write-only");
check(i);
files_[i].read(static_cast<char *>(dst), static_cast<std::streamsize>(n));
if (static_cast<std::size_t>(files_[i].gcount()) != n)
throw std::runtime_error("file_stream_array: short read");
}
void flush(std::size_t i) override
{
check(i);
if (write_mode_)
files_[i].flush();
}
private:
void check(std::size_t i) const
{
if (i >= files_.size())
throw std::out_of_range("file_stream_array index");
}
bool write_mode_ = false;
std::vector<std::fstream> files_;
};
// ---------------------------------------------------------------------------
// Mux wire header (shared with async_mux_stream_array)
// ---------------------------------------------------------------------------
#pragma pack(push, 1)
struct stream_frame_hdr
{
std::uint16_t index = 0;
std::uint32_t length = 0;
};
#pragma pack(pop)
// Synchronous mux is `sync_stream_array` / `mux_stream_array` in
// dpf/net/sync_stream_array.hpp: a blocking face over the async mux backends
// (including TLS), so sync call sites share party_session encryption and
// wire policy.
// ---------------------------------------------------------------------------
// RoundSink adapter (round → stream, with lane reuse when rounds > streams)
// ---------------------------------------------------------------------------
/// @brief Multi-round RoundSink over a synchronous `stream_array`.
/// @details Same wire contract as `async_round_sink`: a plan-shape hello on
/// lane 0 (sent at construction, read before the first peer bytes),
/// unframed lanes when every round has its own lane, and framed
/// `{round, nbytes}` lanes otherwise (or when `framing` says so). A
/// framed round accepts partial instance prefixes. Call
/// `flush_round(r)` before starting another round's pending writes.
class stream_array_sink : public RoundSink
{
public:
stream_array_sink(stream_array & streams,
std::vector<std::size_t> slot_bytes_per_round, std::size_t count = 1,
framing_mode framing = framing_mode::automatic, bool hello = true)
: streams_(&streams),
count_(count),
slot_bytes_(std::move(slot_bytes_per_round)),
map_(streams.size(), slot_bytes_.size(), framing),
windows_(slot_bytes_.size()),
in_(slot_bytes_.size()),
in_filled_(slot_bytes_.size(), 0),
zero_seen_(slot_bytes_.size(), false),
announced_(slot_bytes_.size(), false),
hello_(hello)
{
if (streams_->size() == 0)
throw std::invalid_argument("stream_array_sink: empty streams");
if (count_ == 0)
throw std::invalid_argument("stream_array_sink: count 0");
for (std::size_t r = 0; r < slot_bytes_.size(); ++r)
{
windows_[r] = round_window(count_, slot_bytes_[r]);
in_[r].assign(count_ * slot_bytes_[r], 0);
}
if (hello_)
send_hello();
}
/// @brief Legacy single-round sink on stream `index` (no hello).
stream_array_sink(stream_array & streams, std::size_t index, std::size_t slot)
: streams_(&streams),
count_(1),
legacy_index_(index),
use_legacy_index_(true),
map_(1, 1)
{
slot_bytes_.push_back(slot);
windows_.emplace_back(count_, slot);
in_.emplace_back(count_ * slot, 0);
in_filled_.push_back(0);
zero_seen_.push_back(false);
announced_.push_back(false);
}
std::size_t count() const noexcept override { return count_; }
std::size_t rounds() const noexcept override { return slot_bytes_.size(); }
bool framed() const noexcept { return map_.framed && !use_legacy_index_; }
std::size_t slot_bytes(std::uint16_t round) const override
{
if (round >= slot_bytes_.size())
throw std::out_of_range("stream_array_sink round");
return slot_bytes_[round];
}
void submit(std::uint16_t round, std::size_t index, const std::uint8_t * bytes,
std::size_t n) override
{
window(round).submit(index, bytes, n);
}
bool peer_ready(std::uint16_t round, std::size_t index) const override
{
if (round >= windows_.size())
return false;
if (framed())
{
if (slot_bytes_[round] == 0)
return zero_seen_[round];
return in_filled_[round] >= (index + 1) * slot_bytes_[round];
}
return window(round).peer_ready(index);
}
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
std::size_t n) const override
{
if (framed())
{
if (!peer_ready(round, index))
throw std::logic_error("stream_array_sink: peer not ready");
if (n != slot_bytes_[round])
throw std::invalid_argument("stream_array_sink read size");
if (n != 0)
std::memcpy(out, in_[round].data() + index * n, n);
return;
}
window(round).read_peer(index, out, n);
}
void flush() override
{
for (std::uint16_t r = 0; r < slot_bytes_.size(); ++r)
flush_round(r);
}
void flush_round(std::uint16_t round) override
{
auto & win = window(round);
std::size_t begin = 0;
std::size_t nslots = 0;
const auto * pend = win.pending_out(begin, nslots);
const std::size_t sb = slot_bytes_[round];
const std::size_t stream_i = stream_index(round);
const std::size_t nbytes = nslots * sb;
if (framed())
{
const bool announce = sb == 0 && !announced_[round];
if (nslots == 0 && !announce)
return;
auto frame = pack_round_frame(round, pend, nbytes);
streams_->write(stream_i, frame.data(), frame.size());
streams_->flush(stream_i);
if (announce)
announced_[round] = true;
if (nslots != 0)
win.mark_flushed(nslots);
ensure_hello();
while (sb == 0 ? !zero_seen_[round]
: in_filled_[round] < win.flushed() * sb)
pull_one_frame(stream_i);
return;
}
if (nslots != 0)
{
streams_->write(stream_i, pend, nbytes);
streams_->flush(stream_i);
win.mark_flushed(nslots);
}
if (nslots == 0)
return;
ensure_hello();
std::vector<std::uint8_t> peer(nbytes);
streams_->read(stream_i, peer.data(), peer.size());
win.accept_peer_at(begin, peer.data(), nslots);
}
void poll() override {}
private:
static constexpr std::uint32_t k_magic = 0x48535044u;
static constexpr std::size_t k_fixed = 36;
std::size_t stream_index(std::uint16_t round) const
{
if (use_legacy_index_)
return legacy_index_;
return map_.lane(round);
}
static void put32(std::uint8_t * p, std::uint32_t v)
{
for (int i = 0; i < 4; ++i)
p[i] = static_cast<std::uint8_t>((v >> (8 * i)) & 0xffu);
}
static std::uint32_t get32(const std::uint8_t * p)
{
return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8)
| (static_cast<std::uint32_t>(p[2]) << 16)
| (static_cast<std::uint32_t>(p[3]) << 24);
}
void send_hello()
{
const std::size_t r = slot_bytes_.size();
std::vector<std::uint8_t> h(k_fixed + 4 * r, 0);
put32(h.data(), k_magic);
h[4] = 1;
h[6] = map_.framed ? 1 : 0;
put32(h.data() + 8, static_cast<std::uint32_t>(r));
put32(h.data() + 12, static_cast<std::uint32_t>(map_.n_lanes));
put32(h.data() + 16, static_cast<std::uint32_t>(count_));
const std::uint64_t fp = slots_fingerprint(slot_bytes_);
for (int i = 0; i < 8; ++i)
h[24 + i] = static_cast<std::uint8_t>((fp >> (8 * i)) & 0xffu);
streams_->write(0, h.data(), h.size());
streams_->flush(0);
}
void ensure_hello()
{
if (!hello_ || hello_read_)
return;
hello_read_ = true;
std::uint8_t f[k_fixed];
streams_->read(0, f, k_fixed);
if (get32(f) != k_magic)
throw std::runtime_error("stream_array_sink: peer did not send a "
"sink hello (both ends must use sinks with hello enabled)");
const std::uint32_t pr = get32(f + 8);
const std::uint32_t pl = get32(f + 12);
const std::uint32_t pc = get32(f + 16);
const bool pfr = (f[6] & 1) != 0;
std::uint64_t pf = 0;
for (int i = 0; i < 8; ++i)
pf |= static_cast<std::uint64_t>(f[24 + i]) << (8 * i);
std::string why;
if (pr != slot_bytes_.size())
why += " rounds " + std::to_string(pr) + " vs "
+ std::to_string(slot_bytes_.size()) + ";";
else if (pf != slots_fingerprint(slot_bytes_))
why += " slot widths differ;";
if (pl != map_.n_lanes)
why += " lanes " + std::to_string(pl) + " vs "
+ std::to_string(map_.n_lanes) + ";";
if (pc != count_)
why += " instances " + std::to_string(pc) + " vs "
+ std::to_string(count_) + ";";
if (pfr != map_.framed)
why += std::string(" framing ") + (pfr ? "on" : "off") + " vs "
+ (map_.framed ? "on" : "off") + ";";
if (!why.empty())
throw std::runtime_error(
"stream_array_sink: peer sink disagrees (peer vs this side):" + why);
std::vector<std::uint8_t> rest(4 * pr);
if (!rest.empty())
streams_->read(0, rest.data(), rest.size());
}
void pull_one_frame(std::size_t stream_i)
{
std::uint8_t raw[round_lane_hdr::size];
streams_->read(stream_i, raw, round_lane_hdr::size);
const auto hdr = round_lane_hdr::unpack(raw);
if (hdr.round >= slot_bytes_.size() || map_.lane(hdr.round) != stream_i)
throw std::runtime_error("stream_array_sink: frame for round "
+ std::to_string(hdr.round) + " on lane "
+ std::to_string(stream_i));
const std::size_t sb = slot_bytes_[hdr.round];
if (hdr.nbytes == 0)
{
if (sb == 0)
zero_seen_[hdr.round] = true;
return;
}
if (sb == 0 || hdr.nbytes % sb != 0
|| in_filled_[hdr.round] + hdr.nbytes > count_ * sb)
throw std::runtime_error("stream_array_sink: round "
+ std::to_string(hdr.round) + " frame of "
+ std::to_string(hdr.nbytes) + " bytes does not fit");
streams_->read(stream_i, in_[hdr.round].data() + in_filled_[hdr.round],
hdr.nbytes);
in_filled_[hdr.round] += hdr.nbytes;
}
round_window & window(std::uint16_t round)
{
if (round >= windows_.size())
throw std::out_of_range("stream_array_sink round");
return windows_[round];
}
const round_window & window(std::uint16_t round) const
{
if (round >= windows_.size())
throw std::out_of_range("stream_array_sink round");
return windows_[round];
}
stream_array * streams_ = nullptr;
std::size_t count_ = 1;
std::size_t legacy_index_ = 0;
bool use_legacy_index_ = false;
std::vector<std::size_t> slot_bytes_;
round_lane_map map_;
mutable std::vector<round_window> windows_;
std::vector<std::vector<std::uint8_t>> in_;
std::vector<std::size_t> in_filled_;
std::vector<bool> zero_seen_;
std::vector<bool> announced_;
bool hello_ = false;
bool hello_read_ = false;
};
/// @brief Map index `i` to SCTP stream `i` on one association.
/// @details Synchronous stub: this blocking `stream_array` face has no linked OS
/// backend, so its I/O throws. Protocol code should depend on
/// `stream_array` only; swap in a linked backend without API changes.
/// The real, event-driven SCTP backend already exists on Linux as
/// `dpf::net::async_sctp_stream_array` (see
/// `net/async_sctp_stream_array.hpp`, guarded by `DPF_HAS_LIBSCTP`).
/// It implements `dpf::net::async_stream_array`, still maps index `i`
/// to SCTP stream `i`, and drops in behind
/// `dpf::async::overlapped_byte_protocol` with no protocol changes.
/// Link `-lsctp`. Windows has no supported SCTP path (SctpDrv is
/// unstable), so the async class throws there.
struct sctp_association
{
int fd = -1;
};
class sctp_stream_array : public stream_array
{
public:
/// @brief Reserved for a linked backend (`libsctp` association fd).
explicit sctp_stream_array(sctp_association /*assoc*/)
{
throw std::logic_error("sctp_stream_array: not linked");
}
/// @brief Size-only stub: every I/O op throws until a backend is linked.
explicit sctp_stream_array(std::size_t streams) : n_(streams)
{
if (streams == 0)
throw std::invalid_argument("sctp_stream_array empty");
}
std::size_t size() const noexcept override { return n_; }
void write(std::size_t, const void *, std::size_t) override
{
throw std::logic_error(
"sctp_stream_array: write — SCTP backend not linked (index i -> SCTP stream i)");
}
void read(std::size_t, void *, std::size_t) override
{
throw std::logic_error(
"sctp_stream_array: read — SCTP backend not linked (index i -> SCTP stream i)");
}
void flush(std::size_t) override
{
throw std::logic_error(
"sctp_stream_array: flush — SCTP backend not linked (index i -> SCTP stream i)");
}
private:
std::size_t n_ = 0;
};
} // namespace net
} // namespace dpf
#endif