Serverless, cheaterless Magic: The Gathering

May 22, 2026

If you'd like to check out Ironsmith for yourself, it's available at chiplis.com/ironsmith

If you've ever worked on an online game, or even just complained loudly enough about one, you probably know the usual answer to cheating: put the truth on a server. The server validates moves, rolls the dice, stores hidden state, and tells each client what it's allowed to see. Simple enough. Well, "simple" in the way distributed systems are simple until you actually have to run them.

Ironsmith is me trying to find out how far the opposite direction can go. Can a browser game for Magic-like rules run without an authoritative game server, while still making cheating either impossible to hide or impossible to accept? This is the kind of question that sounds like a neat weekend experiment and then, a few months later, somehow involves zero-knowledge shuffle proofs and a WebAssembly rules engine. Naturally, I kept going.

The short answer is: mostly yes, as long as we're honest about what "cheating" means. A player can still close their laptop. A browser can crash. A bug in the rules engine is still a bug, and cryptography is not going to descend from the clouds to fix my Rust. What the protocol can do is keep a patched client from making an honest browser accept an illegal move, a biased shuffle, a forged reveal, or a different public game state.

Getting there means combining two problems I probably should have known better than to combine: mental poker and machine-readable Magic.

Mental poker and zero-knowledge proofs, briefly

Mental poker is the name for a family of protocols that let people play a card game remotely without trusting a dealer. The problem goes back to Shamir, Rivest, and Adleman in the late 1970s: Alice and Bob are not in the same room, neither of them trusts the other, and both would very much like the other person not to stack the deck. A relatable situation.

The boring solution is to appoint a server as dealer. The fun solution is to encrypt the deck, let each player contribute to the shuffle, and reveal cards only when the rules say they should become visible. No single player gets to choose the deck order, and no player learns hidden cards just because they helped shuffle.

Of course, "trust me, I shuffled these encrypted bytes fairly" is not much of a protocol. A shuffled encrypted deck should look like random data. If the verifier can see the permutation, the shuffle leaks information. If the verifier cannot see anything, the shuffler might cheat. Very convenient, very annoying.

This is where zero-knowledge proofs enter. In this setting, a zero-knowledge shuffle proof lets a client prove that the output deck is a valid permutation and re-randomization of the input deck, without revealing the permutation. The proof says: "I transformed the deck correctly," while hiding: "this old position became that new position."

This isn't just academic trivia. Libraries such as LibTMCG implement trusted-third-party-free card game protocols, and Ironsmith uses the Rust ziffle mental-poker library for multi-party shuffles with zero-knowledge proofs.

Poker is the classic example because poker has a simple deck model and a small rules surface. Magic is what happens when the universe decides you were getting too comfortable.

Why Magic is a hostile target

Magic: The Gathering is a famously large card game, but card count is only the obvious problem. The real problem is that cards are tiny programs written in natural language and interpreted against a living game state.

A card can create a replacement effect. Another can change the rules for when a player may cast spells. Another can reveal the top card of one library to one player, all players, or no one depending on a continuous effect. Cards can copy other cards, change control, change characteristics in layers, set up delayed triggers, ask players to make choices, move hidden objects without revealing them, or reveal objects temporarily and hide them again.

The academic results are correspondingly strange. Churchill, Biderman, and Herrick showed that real Magic is at least as hard as the Halting Problem in constructed positions, in Magic: The Gathering is Turing Complete. Stella Biderman later pushed further in Magic: the Gathering is as Hard as Arithmetic. Wizards maintains the Comprehensive Rules as a reference for the rules and corner cases. A 2026 Scryfall-based analysis counted 33,998 unique card designs as of April 2026 in Thirty Years of Magic Cards, Measured.

The exact count is less interesting than the shape of the problem. Magic behaves more like a mutable programming language for game state than like a fixed deck game, and hidden information is threaded through a lot of the interesting parts.

What Ironsmith is

Before getting to multiplayer, here's what Ironsmith is actually doing.

First, there's the engine. This is the deterministic runtime that tracks players, zones, objects, the stack, priority, turns, choices, mana, damage, combat, triggers, replacement effects, prevention effects, continuous effects, and state-based actions. The engine is the referee. Given the same starting state and the same command stream, every peer should arrive at the same result.

Then there's the parser. Magic cards are written for humans, not machines. Ironsmith tokenizes oracle text, recognizes common grammar, and turns text such as "destroy target creature" or "whenever another creature enters" into typed semantic structures. The parser is not supposed to smuggle behavior through string labels. The project tries to promote card wording into reusable, structured facts.

Finally, there's the compiler. Parsed card text is lowered into engine behavior: spell effects, activated abilities, triggered abilities, static abilities, replacement effects, target requirements, choices, and continuous modifications. Hand-coding every card as a bespoke script would make the project look complete for about ten minutes and then collapse under its own card pool. The useful work is making common Magic language compile into shared engine primitives, so improving support for a mechanic improves many cards at once.

The browser UI runs the Rust engine through WebAssembly, which is where this starts to matter for multiplayer. Every peer can run the same engine locally. There is no need for a private server process to decide whether an action was legal. If a client sends "cast this spell," every other client can replay that command in its own WASM instance and reject it if the engine says it was not legal.

That deterministic engine is the boring foundation. The fun part is what we wrap around it.

The multiplayer problem: Magic changes who knows what

A serverless Magic client has four jobs at the same time:

  1. Was the action legal?
  2. Did the action produce the same public game state for everyone?
  3. Was every newly revealed hidden card the card that had been committed before the game?
  4. Did each player learn exactly the private information they were entitled to learn, and no more?

The first one belongs to the engine. The other three are where the audit protocol starts earning its keep.

In a normal client-server architecture, the server gets to cheat for good reasons. It knows the library order, every hand, every face-down card, every random result, and every private choice. Then it politely redacts whatever each client should not see.

Ironsmith does not give that power to a server. The lobby host helps assemble the match and relay messages, but the host does not get to sequence the game, shuffle a library, roll a die, inspect a hand, or decide which action is canonical. Every browser keeps its own transcript and verifies every action before mutating local state.

Here's where Magic starts being annoying. There isn't one hidden-information rule. There are a lot of hidden-information transitions that look similar until you try to implement them.

A card can move from library to hand without becoming public. A card can move from library to battlefield face up, becoming public. A player can look at the top card of their own library. All players can reveal cards from the top of a library until some condition is met. One player can look at another player's hand. A spell can instruct a player to search a library, reveal one chosen card, then shuffle. A continuous effect can make the top card of a library visible for as long as a permanent remains on the battlefield. A replacement effect can change where a hidden card goes. A delayed trigger can reveal something later.

A single "show the card" flag cannot model all of that. The protocol has to preserve the information boundary across each engine transition.

Ironsmith handles that by making the engine ask for receipts.

When the WASM engine applies a command, it knows what hidden-information boundary the rules just crossed. Instead of merely saying "state changed," it can say, in effect:

The browser audit layer names these cases as public_open, private_open, public_view_window, private_view_window, and hidden_move.

That vocabulary is the bridge between Magic's effect system and the cryptographic transcript. The engine decides what the rules require. The audit layer decides what proof must be attached before honest peers accept the action.

Match genesis: committing before anyone acts

Before the game starts, everyone does the boring paperwork that makes the rest of the protocol possible.

Each player has an audit signing key. It signs genesis records, actions, quorum votes, randomness commitments, randomness reveals, timeout votes, disconnect votes, and resync envelopes.

Each player also has an encryption key for private views. This is how hidden-card material can be sent only to the player who is actually allowed to see it.

Finally, each player has a ziffle key for mental-poker shuffling. It comes with an ownership proof bound to the match context, so another player cannot silently swap in a different key later.

Each deck is also committed. For every main-deck slot, the owner creates a salted commitment to the card in that slot. The public deck manifest contains the owner, counts, decklist commitment, commitment root, and one commitment per slot. The private manifest keeps the card names and salts needed to open those slots later.

The useful bit is timing: the commitments exist before the game starts. If a player later reveals that slot 17 was Lightning Bolt, every peer can recompute the commitment from the match id, owner, slot, card name, and salt. If it does not match the genesis commitment, the reveal is rejected.

So a client does not get to decide what a hidden card was after seeing how the game developed. That would be a very powerful draw spell.

Libraries: no trusted shuffle

Deck commitments prove what the deck contains. They do not prove the library was shuffled fairly, so we still need to take the final order out of the deck owner's hands.

For each player's library, Ironsmith runs a ziffle shuffle ceremony. The deck starts as encrypted original slots. Then every player contributes a shuffle step in deterministic player order. Each step produces a new encrypted deck and a zero-knowledge proof that the new deck is a valid shuffle of the previous one.

No single player controls the final order. If at least one participant honestly contributes entropy, the deck order is not chosen by the deck owner. And because the shuffle proofs hide the permutation, helping shuffle an opponent's library does not reveal that library.

Later, when a card at a shuffled position must be opened, the relevant ziffle reveal links the shuffled position back to the committed original slot. The opening still has to satisfy the ordinary deck commitment. This gives the protocol two layers of evidence:

Here's the core mental-poker move inside Ironsmith: the engine never needs a server-owned array called "Alice's library." It consumes hidden library objects backed by a jointly shuffled commitment structure, with proofs attached for the parts that would otherwise require trust.

Actions: signed commands plus local replay

Once the match starts, every game action travels with paperwork of its own: a signed audit envelope.

The envelope commits to the match id, sequence number, acting player, previous state hash, exact command, clock audit, hidden-card openings, randomness reveals, shuffle proofs, private-view proofs, the resulting public checkpoint hash, and the next audit state hash.

Those bytes are canonicalized before hashing or signing. This is one of those details that sounds boring until it breaks everything. If two browsers serialize the same logical object differently, the hashes stop matching. Ironsmith signs canonical JSON: sorted keys, normalized values, domain-separated hashes, and canonical low-S ECDSA signatures.

When a peer receives an action, it does not ask "did this come from the host?" It asks a stricter set of questions:

If any answer is no, the action does not become local state.

So the message is no longer just "the client sends a move." It is closer to "the client sends a transition plus the evidence needed to replay it." A malicious browser can still send arbitrary bytes. It cannot make another honest browser mutate state unless those bytes survive signature verification, transcript verification, engine replay, crypto proof checks, and public checkpoint consensus.

Public openings: when hidden becomes visible to all

The easiest case is public_open: a hidden object becomes public.

Examples include a card being revealed, milled face up, cast from a hidden zone, or otherwise moved into a public zone with its identity exposed. The engine emits a requirement saying which hidden object must be opened. The action audit must include an opening for that object.

For a deck card, that opening contains enough material to prove the identity: the owner, committed slot, card name, salt, and, when the card came from a ziffle-shuffled library position, the position-opening proof that links the runtime library position to the committed slot.

Every peer verifies the opening. If the card says it is Lightning Bolt but the salted commitment says otherwise, the action is invalid. If the card came from a library position but the ziffle proof does not link that position to the slot, the action is invalid. If the opening is missing, the action is invalid.

The public checkpoint then includes the card identity, because everyone is now entitled to know it.

Private openings: when only one player may know

Drawing a card is where things get less friendly.

When Alice draws from her library, Alice must learn the card. Bob must not. Bob still needs confidence that Alice did not choose the card, invent the card, or draw from a different hidden position. That's the whole trick.

Ironsmith calls that a private_open. The action transcript includes a proof object that is publicly bound to the action, but the actual card material is encrypted to the authorized viewer's encryption key. Alice can decrypt it and verify the card against the deck commitment. Bob cannot decrypt the card name, but he can verify the signed envelope, the public parts of the proof, the engine transition, and the resulting public checkpoint.

The non-viewer does not trust Alice's UI. They verify a narrower statement: the action carried a private-view proof bound into the signed transcript, addressed to the legal viewer, and the public state after the action matches local replay. If the match is later exported with disclosures, the encrypted private openings can be checked after the fact.

During the game, that separation preserves secrecy. After the game, it gives us something we can audit.

View windows: Magic often reveals sets, not single cards

Many Magic effects do not reveal or inspect one card. They create a temporary view over a set of hidden cards, because of course they do.

"Look at the top three cards of your library." "Reveal cards from the top of your library until you reveal a creature." "Target opponent reveals their hand." "You may play with the top card of your library revealed." These are different rules, but they all require the engine to describe a window of visibility.

Ironsmith treats those as view windows.

A public_view_window means every player is entitled to see the batch. The audit must include public openings for the relevant objects, and the resulting public checkpoint can expose them for as long as the rules say they remain visible.

A private_view_window means only a specific viewer is entitled to see the batch. The proof material is encrypted to that viewer, but the transcript still commits to the window. Other players can verify that a private view happened under the signed action and that the public state after the action is the one their engine replay produced.

Splitting view windows this way lets the protocol model the annoying middle ground between "everyone sees" and "nobody verifies." The engine describes who may see what. The cryptographic layer enforces that description.

Hidden moves: preserving identity without revealing it

Some transitions move a hidden card without revealing it, which is exactly as fun to implement as it sounds.

A library card can move to hand. A face-down object can move between zones. Cards can be reordered, tucked, shuffled, manifested, or otherwise transformed while their identity remains hidden from at least some players.

For these cases, opening the card would be a privacy bug. Doing nothing would be an audit bug, because the hidden object still needs continuity. The system has to know that the object after the move is the same committed hidden object as before the move, or an honestly derived hidden object according to the engine's rules.

hidden_move covers that case. Instead of revealing card identity, the engine tracks hidden-card metadata through the transition: owner, hidden slot or runtime commitment, zone, and the public redaction needed for the checkpoint. Peers can agree that a hidden object moved without learning what the object is.

The public checkpoint intentionally redacts hidden identities. Think of it as a public-state fingerprint rather than a full state dump: enough to prove that everyone agrees on life totals, visible objects, zones, stack, turn structure, choices, and redacted hidden-object positions, without leaking card names that are still private.

Randomness outside libraries

Libraries are not the only source of randomness. Magic-like games can require random choices, shuffles of derived groups, or other non-library random events. I refuse to say "coin flip" here because if I do, some card I forgot about will immediately need six other cases.

Ironsmith handles non-library randomness with signed commit/reveal. Each player first signs a commitment to a random nonce. After all commitments are present, players reveal their nonces. The combined seed is derived from all valid reveals. Since no player knows the other nonces when committing, no single client can choose the final outcome after seeing everyone else's input.

The reveal transcript is attached to the action that consumes the randomness. Peers verify the signatures, the nonce-to-commitment matches, and the derived seed.

Same pattern as before: don't trust one participant to be the source of truth. Make the thing that changes state carry the evidence needed to verify it.

Quorum, forks, and what the protocol does not promise

For three- and four-player games, actions also carry peer quorum certificates. A quorum vote signs the action's match id, sequence, actor, previous hash, next hash, public checkpoint hash, and action signature. Peers refuse to sign conflicting votes for the same sequence.

The engine already validates actions. Quorum is about host sequencing authority. The host may relay messages, but the host should not be able to unilaterally decide which action everyone treats as the next canonical transition.

There's still an important limit, and it's worth saying out loud before someone in the comments does it for me: a peer-to-peer protocol cannot prevent every form of liveness failure. A malicious player can disconnect. A malicious player can refuse to provide a reveal. A malicious player can try to fork the action log by sending different signed messages to different peers. The protocol does not make those behaviors physically impossible. It makes them rejectable or provable.

If a fork appears, the transcript can carry the two signed actions with the same sequence and previous hash. That becomes dispute evidence. Honest clients can show which actor equivocated, and in quorum games, which voters signed both branches.

So "cheaterless" has to be read pretty narrowly. Ironsmith does not make bad network behavior disappear. It makes accepted state transitions verifiable. If someone cheats by sending invalid data, honest peers reject it. If someone equivocates, the signatures expose it. If someone disconnects, the protocol can move toward timeout or forfeit policy, but it cannot force their machine to keep participating.

Resync and postgame verification

Peer-to-peer games also need resync, because browsers refresh, connections drop, and people click things they shouldn't. I would love to pretend otherwise, but I have used computers before.

Ironsmith's resync payload is not "trust me, here is the state." It is a signed transcript segment plus a checkpoint. The receiving client verifies the transcript hash chain, validates signatures and quorum certificates, verifies the resync envelope, then replays the actions through its local engine. Only if the replayed public checkpoint hash matches the signed checkpoint does the client accept the resync.

The same principle applies after the match. A complete audit transcript can be verified later, including engine replay and, when available, private-view disclosures. That gives us a debugging artifact and a way to inspect disputes without asking anyone to trust a screenshot of a game state.

The transcript is the artifact. The server, if there is one around for signaling or relaying, is just plumbing.

Why the architecture fits Magic

The design choice that makes this manageable is avoiding one giant cryptographic protocol for "a game of Magic." That would be brittle, and I already have enough brittle things in my life. Magic has too many effects, too many information boundaries, and too many interactions.

Instead, the deterministic engine remains responsible for rules. It decides what actions are legal and what information changes when they resolve. The audit layer remains responsible for evidence. It checks that every hidden-card opening, private view, shuffle proof, random reveal, action signature, quorum vote, and checkpoint hash matches the transition the engine produced.

The split gives each layer a job:

After that decomposition, the server has very little left to do. Legality comes from deterministic replay. Hidden-card honesty comes from commitments and openings. Library fairness comes from multi-party zero-knowledge shuffles. Randomness comes from commit/reveal. Private information comes from per-viewer encryption. Public consensus comes from redacted checkpoint hashes. Fork accountability comes from signed transcripts.

That's what the Ironsmith multiplayer code is trying to buy: fewer places where "the JavaScript client said so" is enough. The host can help peers find each other, but it does not secretly become the referee. A patched client can lie, but the lie has to survive everyone else's engine and every commitment already made.

For Magic, that is the interesting part. The protocol follows the same boundaries the rules engine already cares about: actions, hidden objects, public views, private views, randomness, shuffles, and replayable state. The closer those boundaries line up, the less room there is for a cheater to hide in the gap between "what happened" and "what everyone can verify."