Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Coins, not accounts: the unspent-output model (Nakamoto 2008)#
Bitcoin keeps no balances. Value lives in coins, the outputs of earlier transactions, each locked to an owner. A payment consumes whole coins as inputs and creates new outputs: one to the payee, usually one back to the payer as change. Whatever is not assigned to an output goes to the miner as a fee. A coin can be spent once; a double spend is two transactions consuming the same coin.
What to look for#
Alice pays Bob 30 from a 50 coin: she gets 18 back as change and 2 goes to fees. Spending the same coin again is rejected, because it is no longer in the unspent set.
The history behind this experiment: Breakthroughs in Authenticated Data Structures.
Spend a coin, receive change#
import matplotlib.pyplot as plt
import blockchainkit as bk
alice_key, bob_key = 7, 11
alice = bk.structures.address(bk.crypto.public_key(alice_key))
bob = bk.structures.address(bk.crypto.public_key(bob_key))
utxos = bk.structures.UTXOSet.genesis({alice: 50})
(coin,) = utxos.coins_of(alice)
payment = bk.structures.UTXOTransaction((coin,), ((bob, 30), (alice, 18))).signed([alice_key])
after = utxos.apply(payment)
print(f"Bob {after.balance(bob)}, Alice {after.balance(alice)}, fee {payment.fee(utxos)}")
assert (after.balance(bob), after.balance(alice), payment.fee(utxos)) == (30, 18, 2)
Bob 30, Alice 18, fee 2
A coin cannot be spent twice#
double_spend = bk.structures.UTXOTransaction((coin,), ((alice, 50),)).signed([alice_key])
try:
after.apply(double_spend)
except ValueError as error:
print("Rejected:", error)
Rejected: OutPoint(aeebad4a796f..., 0) is not an unspent output
Bob combines coins#
second = bk.structures.UTXOTransaction((after.coins_of(alice)[0],), ((bob, 18),)).signed(
[alice_key]
)
after2 = after.apply(second)
merge = bk.structures.UTXOTransaction(after2.coins_of(bob), ((alice, 48),)).signed(
[bob_key, bob_key]
)
final = after2.apply(merge)
assert final.balance(alice) == 48 and len(final) == 1
stages = {"genesis": utxos, "pay Bob": after, "change to Bob": after2, "Bob merges": final}
fig, ax = plt.subplots(figsize=(8, 3.5))
for row, state in enumerate(stages.values()):
left = 0
for outpoint in sorted(state.coins_of(alice) + state.coins_of(bob)):
coin_ = state[outpoint]
color = "#2563eb" if coin_.owner == alice else "#ea580c"
ax.barh(row, coin_.amount, left=left, color=color, edgecolor="white")
left += coin_.amount
ax.set(
yticks=range(len(stages)),
yticklabels=list(stages),
xlabel="value (blue: Alice, orange: Bob)",
title="Unspent coins after each transaction",
)
ax.invert_yaxis()
fig.tight_layout()

Exercise#
In the UTXO model, how does a wallet compute “its balance”? Why can two transactions that spend different coins of the same owner be validated in parallel, while two account-model transfers from one sender cannot?
Total running time of the script: (0 minutes 0.221 seconds)