Skip to content

Developers

Private Ephemeral Rollups

Hidden state with MagicBlock: delegation, permissions, randomness.

  1. Base layer

    Match opens in tesoro_core.

  2. Delegate

    State moves to the TEE.

  3. Permissions

    Who may read each account.

  4. Play

    Game rules run in the rollup.

  5. Commit

    Wipe hidden state, commit back.

  6. Settle

    CPI into tesoro_core.

A Match's result must reach the Solana settlement program without trusting a Studio server's signature, and some of its state must stay hidden: the Hold'em deck, a Player's hole cards, a Block Puzzle run, a Trading Duel position. ADR 0003 decides that each Game is a Solana program whose Match state is delegated to a MagicBlock Private Ephemeral Rollup, which runs in an Intel TDX trusted execution environment (TEE).

Lifecycle of delegated state

  1. Base layer. Players open and join the Match in tesoro_core. The Game creates its state accounts (Session and Boards, or a table, or a Duel).
  2. Delegate. The Game's state accounts are delegated to the TEE validator. Anyone may send the delegation transaction.
  3. Permissions. Permission accounts declare who can read each delegated account. Inside the rollup, an account with a permission is unreadable to non-members.
  4. Play. Game instructions run in the rollup against the delegated accounts, with the Game's own clocks and rules.
  5. Commit and undelegate. Hidden state is wiped, permissions are closed, and the final state is committed back to the base layer.
  6. Settle. On the base layer, anyone calls the Game's settle instruction, which CPIs into tesoro_core (see How settlement works).

Rollup (TEE) transactions are private and have no public explorer page. The Game's evidence files in the repository (evidence.json) record their signatures.

Delegation is pinned to one validator

An early review found that anyone could delegate a Game's state to a validator they controlled and commit a forged result (audit F-01, Critical). The fix has two parts. The delegation program keeps a ProgramConfig per Game program whose approved_validators lists only the MagicBlock TEE validator, and the Game programs also pin the validator themselves. A delegation naming any other validator fails with InvalidValidator (custom error 6020). The ProgramConfig addresses are on Programs and addresses.

What is hidden in each Game

GameHiddenHow it is hiddenWhat becomes public
Block PuzzleThe Seed, then each Player's Board (moves)A SeedVault with a permission that has zero members. The Seed is written there only by the VRF callback and copied into a Player's own Board by begin_run, in the transaction that starts their 90 s clock. Each Board's only member is its Player.After seal and commit: both Boards. The SeedVault is wiped.
Hold'emThe Deck and each Player's hole cardsDeck: zero-member permission. Hole accounts: readable by their Player. The seed of a hand folds both Players' fresh secrets and the VRF output.Board cards as dealt; hole cards only at a showdown. close_match wipes the Deck and Holes before committing.
Trading DuelEach Player's Book: open position and fillsBook readable only by its Player. The public Duel carries a coarse lead and the trade counts.After the Match is committed: both full fill histories.

Randomness

Randomness comes from MagicBlock's VRF, delivered by callback straight into private state inside the TEE, so a public VRF value cannot expose it. Hold'em additionally folds both Players' own per-hand secrets (contribute_entropy) into the seed: seed = H(entropy || vrf || hand_no).

Score is a replay, never reported

No Block Puzzle instruction takes a score. play feeds each (piece, x, y) to the deterministic engine on the Board's own state, illegal moves revert the batch, and the packed move log is stored so anyone can re-verify. A bit-exact TypeScript port of the engine (app/src/games/block-puzzle/engine.ts) lets the app preview the same result.

Trading Duel prices

Fills execute at MagicBlock's real-time pricing oracle (Pyth Lazer pushed into rollup accounts every 50 to 200 ms). The program reads the PythPriceUpdateV2 bytes itself. Anti-latency rules: a 5 bps spread on every fill and rejection of any price older than 2 seconds. The end price is pinned: end_match needs a price published within [ends_at, ends_at + 4 s], otherwise it settles at the last mid recorded during the window, so waiting cannot change the result. Maintenance margin is 50 bps and a liquidated position loses at most its margin.

Liveness

Clocks live in the programs: Block Puzzle 90 s per run, an opener window of one hour and a joiner window of 24 hours; Hold'em 30 s per action and 60 s to contribute entropy, with three missed turns in a row a forfeit; Trading Duel a five-minute window. claim_timeout, seal, commit_boards, finalize and settle can be sent by anyone, and the keeper sends them when no Player does.

Test mode · simulated yield · 1 month = 10 min · nothing here has value