Developers
Private Ephemeral Rollups
Hidden state with MagicBlock: delegation, permissions, randomness.
- Base layer
Match opens in tesoro_core.
- Delegate
State moves to the TEE.
- Permissions
Who may read each account.
- Play
Game rules run in the rollup.
- Commit
Wipe hidden state, commit back.
- 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
- 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).
- Delegate. The Game's state accounts are delegated to the TEE validator. Anyone may send the delegation transaction.
- Permissions. Permission accounts declare who can read each delegated account. Inside the rollup, an account with a permission is unreadable to non-members.
- Play. Game instructions run in the rollup against the delegated accounts, with the Game's own clocks and rules.
- Commit and undelegate. Hidden state is wiped, permissions are closed, and the final state is committed back to the base layer.
- 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
| Game | Hidden | How it is hidden | What becomes public |
|---|---|---|---|
| Block Puzzle | The 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'em | The Deck and each Player's hole cards | Deck: 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 Duel | Each Player's Book: open position and fills | Book 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.