libdpf/doc/pages/experiment.md
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

8.8 KiB
Raw Permalink Blame History

Logging, statistics, and experiments

Paper and production runs need three things the bare key API does not: leveled run logs, granular cost statistics, and replayable coins. All three live in the same measurement stack — [dpf/log.hpp](@ref dpf/log.hpp), [dpf/run_log.hpp](@ref dpf/run_log.hpp), [dpf::experiment](@ref dpf/experiment.hpp), and the [prg::count](@ref dpf/prg_count.hpp) / [thread_work](@ref dpf/thread_work.hpp) counters.

Uninstrumented code stays quiet and keeps reading /dev/urandom. Logging and experiments are opt-in. run_measured, the battery, and party_node call [app::start_logging](@ref dpf/run_log.hpp) and install an experiment covering the thread that constructs it. run_parties, measure_plan, and the battery give party i its own stream, ex.derive_party(i), whose master is SHA-256 of the experiment's master and i, and they use it for every trial, warmup included. A kernel handed to a compute pool draws from its party's stream. Replaying the master replays every party, as long as each party makes the same draws in the same order. The party masters are noted as p0/master, p1/master, and so on.

Run log

[dpf/log.hpp](@ref dpf/log.hpp) writes one key=value line per event to stderr, syslog, or a file. Nothing is written until log::configure runs (normally through app::start_logging), so library code can log freely and a test that never configures the log stays quiet. Every line starts with ts, lvl, inv (invocation id), pid, tid, then role=<party> when the thread has one, then ev=<event> and the event's fields. Seed bytes go through record::seed as hex, as a SHA-256 fingerprint, or not at all.

[app::start_logging](@ref dpf/run_log.hpp) turns it on and writes the provenance banner first, at info:

Event Contents
ev=start program, cwd, UTC/local start, invocation id shared with runs.csv
ev=build git rev (LIBDPF_GIT_REV), compiler, NDEBUG, ISA, sanitizers
ev=host hostname, kernel, CPU model, affinity, governor, turbo, memory, load; ev=if per interface
ev=argv shell-quoted command line
ev=env DPF_* / LIBDPF_* and perf-relevant vars (LD_PRELOAD, …)
ev=config every run_config setting

After that come seeds with their source, listeners and links (endpoints, peer authentication, encryption, applied socket options, kernel RTT), each party's plan and costs, trial statistics, and failures. ev=end at exit gives elapsed time. The settings are run_config keys:

DPF_LOG_LEVEL=debug  DPF_LOG=file:/tmp/run.log,stderr  DPF_LOG_SEEDS=hash
--log_level=debug    --log=syslog                      --log_seeds=off

Levels are silent, error, warning, info (the default), debug, and trace. With log_seeds=full the log holds every master, so anyone who has the file can regenerate that party's randomness. hash prints a SHA-256 fingerprint instead.

Replayable seeds

dpf::experiment ex("keyword_pir");          // fresh 32-byte master
auto master = ex.seed();                    // record for the paper
auto x = dpf::uniform_sample<std::uint64_t>();

auto again = dpf::experiment::replay("keyword_pir", master);
assert(dpf::uniform_sample<std::uint64_t>() == x);

While the experiment is installed, every uniform_fill / uniform_sample draw on that thread (DPF roots, Beaver default sampler, pads, Shamir, IKNP, field rejection) comes from AES-CTR of the master. The master itself is always drawn from OS entropy, even when nested under another experiment.

Constructors that already own a seed note it automatically:

Type Seed name
(master) master
beavers::oracle beavers::oracle (+ lane_table)
buffered_prg buffered_prg
prg_pad_rng prg_pad_rng
pseudorandom_root_sampler pseudorandom_root_sampler

Call ex.note_seed("label", value) for anything else. Replay is order-sensitive: the same party must make the same uniform_sample calls. Indexed beavers::oracle(seed) stays the seekable Beaver path; its seed still appears in the report when constructed under the experiment.

Measuring a compose plan

auto plan = dpf::protocol::fss_point_plan(0);
auto ex = dpf::app::measure_plan("fss_point", plan);
ex.write_csv("/tmp/run1");

Or from an application sketch:

// Prints rounds, live bytes, wall/CPU, PRG evals, random bytes, seed hex.
// Set DPF_EXPERIMENT_DIR=/tmp/run1 to also emit CSVs.
dpf::app::run_measured("keyword_pir",
    dpf::protocol::keyword_pir_compose_plan(0, depth), 2);

What is recorded:

Metric Source
Interactive rounds / DAG depth plan::exchange_waves() / plan::waves()
Critical path back-walk of max-wave_of inputs
Schedule bytes per edge slot_bytes + wave_channel
Bytes out/in per round the round's send and receive slots, on its own channel
Wall / CPU steady_clock / party 0's thread CPU plus its pool kernels' CPU
Symmetric-key blocks prg::count(purpose, primitive) per thread
Random bytes uniform_fill TLS counter
Seeds master + every note_experiment_seed

Symmetric-key blocks are counted by purpose and primitive. The purposes are expand (tree expansion, leaf conversion, label and column PRGs), hash (IKNP row hashes, garbled-gate hashes), and harness (the experiment's own seed stream). The primitives are AES-128, AES-256, ChaCha, and LowMC. prg_evals is expand plus hash over every primitive. Harness blocks are never part of it. The counters are per thread; a kernel a party hands to a compute pool (compute_threads > 0) is charged to that party when it returns ([dpf/thread_work.hpp](@ref dpf/thread_work.hpp)).

CSV layout

write_csv(dir) appends. Every row ends with the id of the process that wrote it (invocation), so rows from different runs into one directory stay apart. A file whose header differs from this layout is renamed to <name>.before-<UTC>.csv before new rows go in.

  • summary.csv — one row per run, including master_seed hex. wall_ns, cpu_ns, and the counters come from the last (instrumented) trial; median_ns and slowest_median_ns are medians over the timed trials for party 0 and for the slowest party.
  • rounds.csv — per-round deltas and edge name
  • edges.csv — protocol totals per peer / rss_next / dealer
  • seeds.csv — every noted seed as hex
  • critical_path.csv — node id, wave, opcode, effect
  • config.csv — every run_config setting
  • trials.csv — every party's wall time for each timed trial
  • wire.csv — party 0's link counters, headers included
  • sym.csv — symmetric-key blocks by purpose and primitive
  • runs.csv — the invocation id that ties rows to the run log, and whether the master was fresh, provided, or derived

Before any party starts its clock, all parties wait at a start gate until every one has finished setup.

The battery

The harness is party_bench and profile_party. Both spawn p0, p1, and p2 and dial [net::trio](@ref dpf/net/trio.hpp), the library's localhost mesh (p0-p1, p0-p2, p1-p2). A flow's bytes and rounds are the frames on that mesh. The repeat barrier is not included.

party_bench --tag bench --repeat 3 --warmup 1
party_bench --case arith_mul_p11 --repeat 5
profile_party --suite gadget --repeat 3 --warmup 1

--tag bench is the default list. The gadget rows are tagged bench as well, so they sit on that list. profile_party --suite gadget runs only those rows. profile_party --suite all appends them after the core and extreme suites.

A secret index stays a DPF. The gadget rows time what you do after a key, or on shares the parties already hold:

  • arith_proj_*, arith_mul_*, arith_thresh_*, arith_chain_mul4 — Ball–Malkin–Rosulek word garbling. Party 0 sends the evaluator view on the p0–p1 link. Party 1 evaluates that view and opens.
  • yao_if_*, yao_onehot_* — stacked and one-hot garbling on the garbler. The active branch's tables and the evaluator's labels go across the p0–p1 channel, and party 1 evaluates those bytes. Base OT stays on the iknp tag.
  • flute_d* — FLUTE. Party 2 deals the mask shares. Parties 0 and 1 exchange the online bits and open.
  • shuffle_n* — three hidden-shuffle passes. Each pass's array is sent on the ring and consumed as the next party's inbound. Party 0 opens the sum.

Sizes are the modulus, the branch width, the table width, and the column length. The manual pages are [a small word](@ref arith_garble_word), [a public table](@ref flute_lut), [a secret branch](@ref yao_stack), and [an array of shares](@ref share_shuffle).

See also [Network, parties, and MPC](@ref network_and_mpc), [Protocol composition](@ref protocol_compose), and the [application mockups](@ref applications).