Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Plasma: child chains secured by exit games (Poon and Buterin, 2017)#
Plasma runs a whole chain off chain. An operator produces its blocks and posts only their Merkle roots to a root-chain contract, which cannot check the transactions behind them. Coins stay safe because their owners can always exit: prove ownership on the root chain, then wait out a challenge period during which anyone can prove the exit wrong.
In Plasma Cash every coin has its own slot in each block’s sparse Merkle tree. An exit shows the coin’s last transfer and the one before it, and is cancelled by a later transfer signed by the exiting owner (it spent the coin) or by a different transfer signed by the previous owner in between (the exiting transfer was a double spend). An exit stands only if
A coin changes hands three times#
chain = bk.channels.PlasmaChain(period=7)
deposit = chain.deposit(public_key(ALICE)) # Block 1.
to_bob = chain.submit_block([bk.channels.sign_transfer(ALICE, 0, public_key(BOB), 1)])
to_carol = chain.submit_block([bk.channels.sign_transfer(BOB, 0, public_key(CAROL), to_bob)])
timeline = []
# Bob tries to exit the coin he already gave Carol.
stale = chain.start_exit(chain.prove(0, to_bob), deposit, height=10)
assert chain.challenge(stale, chain.prove(0, to_carol), height=12)
timeline.append(("Bob exits a spent coin", 10, 12, "challenged"))
# Alice signs the coin away a second time; a colluding operator includes it.
to_dave = chain.submit_block([bk.channels.sign_transfer(ALICE, 0, public_key(DAVE), 1)])
forged = chain.start_exit(chain.prove(0, to_dave), deposit, height=20)
assert chain.challenge(forged, chain.prove(0, to_bob), height=23)
timeline.append(("Dave exits a double spend", 20, 23, "challenged"))
# Carol, the rightful owner, exits unchallenged.
rightful = chain.start_exit(chain.prove(0, to_carol), chain.prove(0, to_bob), height=30)
assert not chain.challenge(rightful, chain.prove(0, to_dave), height=31)
assert chain.finalize(rightful, height=37).owner == public_key(CAROL)
timeline.append(("Carol exits her coin", 30, 37, "finalized"))
print([status for *_, status in timeline])
['challenged', 'challenged', 'finalized']
Why a withholding operator forces mass exits#
If the operator stops publishing blocks, each owner must exit each coin on the root chain, within the challenge period.
coins = [1, 10, 100, 1_000, 10_000]
exits_per_block = 50 # What the root chain can absorb per block, say.
blocks_needed = [c / exits_per_block for c in coins]
fig, (left, right) = plt.subplots(1, 2, figsize=(11, 4))
for row, (_, start, end, status) in enumerate(timeline):
color = "#16a34a" if status == "finalized" else "#dc2626"
left.barh(row, end - start, left=start, color=color)
left.text(end + 0.5, row, status, va="center")
left.set_yticks(range(len(timeline)), labels=[label for label, *_ in timeline])
left.set(xlabel="root-chain height", xlim=(0, 50), title="Three exits of one coin")
right.loglog(coins, blocks_needed, "o-", color="#d97706")
right.axhline(7, color="black", linestyle="--", label="challenge period")
right.set(xlabel="coins on the child chain", ylabel="root-chain blocks to exit them all")
right.set_title("Mass exit after the operator disappears")
right.legend()
fig.tight_layout()
plt.show()

Exercise#
Plasma Cash also lets a challenger accuse an exit of an invalid
history, which the exiting owner answers with a later transfer. Sketch
that third challenge on
PlasmaChain: what must the
challenger show, and what must the owner answer with?
Total running time of the script: (0 minutes 0.213 seconds)