zPredictdocs
App
Menu

Circuits

View .md

The Garble clearing circuit (orders, balances, credentials, withdrawals), the resolution circuit, and what they cost in gates and seconds.

packages/circuit holds two programs in the Garble language, run as 3-party MPC with polytune (vendored at rev 47dc378 in vendor/polytune, with preprocessing split from the online phase). Each relayer is one party and feeds in only its own shares. The plaintext references are in packages/core; differential tests check the circuits against them.

Clearing (clear.garble.rs)

Inputs. Each party's shares of a fixed-size batch of orders (DEFAULT_SLOTS = 8, up to MAX_SLOTS = 21, padded with dummies), of the whole balance table (id | balance | key share, DEFAULT_ROWS = 32, 48 on the demo), of the withdrawal requests (DEFAULT_WITHDRAWALS = 2), and its private mock flags. Public: the open market ids (DEFAULT_MARKETS = 6 slots) and a credit per row (deposits and judge credits, which every relayer already sees). If the parties disagree on any public input, nothing moves.

Inside, with no data-dependent control flow:

  1. Rebuild orders (s0 ^ s1 ^ s2) and the table; add credits.
  2. Credentials: rebuild each row's 128-bit key from the three key shares. An order or withdrawal uses a row only if its rebuilt tag equals that key (~21k AND gates at 8 slots × 32 rows).
  3. Withdrawals first: each is taken only if its row covers it and no party flagged the row as mock. A public bit per slot says whether it went through.
  4. Reserve: in batch order, each order reserves its worst case (size × 2 × price half-ticks) from its row. Unfunded, unknown market or malformed → dummy.
  5. Cross within each market: YES p against NO q when p + q ≥ 100, at the midpoint. Instead of sorting, each side's sizes are laid end to end and a YES/NO pair fills by the overlap of their intervals (N×N comparisons); once a pair stops crossing, every later pair does too.
  6. Debit rows what their orders actually paid.

Outputs. Each party's XOR share of every slot's 10-byte fill record and every row's new balance, the withdrawal bits, and one public clearing price per market (volume-weighted YES cost, rounded down). Unfilled and dummy slots are all-zero records, indistinguishable from filled ones to a relayer.

Resolution (resolve.garble.rs)

When all three nodes agree a market resolved and on its outcome, a second, small program runs over every position (each batched order share with its fill share), DEFAULT_POSITIONS = 16 per run: one unit per winning contract goes into the row whose key equals the order's tag, as shares. Nobody learns which positions were in that market, filled or won. Reference: zpredict_core::resolve.

Cost

Three processes over localhost on an M4 Pro, release build (cargo run --release -p zpredict-circuit --example bench <runs> <rows> <slots>), from lib.rs and deploy/README.md:

Slots × rowsMarketsAND gatesPreprocessingOnlineCompile
8 × 324290,5435.7 s0.45–0.53 s6.5 s
8 × 484385,3218.0 s0.61–0.77 s17 s
8 × 486393,999+3%same
8 × 644480,28710.0 s0.78–0.97 s28 s
Resolution, 16 positions × 32 rows158,335~3.5 s total

Crossing costs ~N² AND gates; each table row ~8k, each market slot ~4.3k. Each party uses about one core. Peak RSS at 48 rows (Linux): r0 ~1.1 GB, r1/r2 ~0.55 GB.

Preprocessing ahead. About 92% of a run is input-independent preprocessing, so the relayers prepare one run ahead and hold it (#94); a window with orders runs only the online part. A held run is used only if all three hold the same one. On the hosted VPS (~5.7× slower than the M4 Pro) a fill shows about 25 s after the order, mostly waiting for the window and the run.

Tests

cargo test -p zpredict-circuit                      # differential + resolution tests
cargo test -p zpredict-circuit a_relayer_cannot_act_as_an_account
cargo test -p zpredict-circuit mock_rows_never_withdraw

tests/differential.rs checks the circuit against clear_with_balances (circuit_matches_clear_with_balances, other_shapes_match), that a relayer can't act as an account, that mock rows never withdraw, and that disagreeing public inputs are an error; tests/resolution.rs does the same for resolve.