How do you know who has what?
A ledger is a list of every transaction in the order it happened. Nobody stores a balance. You work each balance out by replaying the list from the start.
A double spend is an attempt to use the same funds twice. A ledger that replays in one agreed order refuses the second.
By the end of this lesson
Note
I can work out balances from a list of transactions and refuse an overspend.
Key words
| Word | What it means |
|---|---|
| Ledger | an ordered list of every transaction |
| Transaction | one payment from one account to another |
| Balance | what an account holds after the list so far |
| Replay | to go through the list again from the start |
| Double spend | using the same funds in two payments |
The cells students fill in are left blank in this preview.
Balances are worked out by replaying payments
A ledger stores only transactions, such as "Asha pays Ben 4". To find a balance, start from the opening balances and apply the transactions one at a time, in order.
Nobody can quietly edit a stored balance, because none is stored. If the list is the same, every copy of the ledger gives the same balances.
Replay four payments in the lab
docker exec jupyter python3 steps/balances.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Worked example: the lab's four payments
| Step | Payment | Balances afterwards | Result |
|---|---|---|---|
| Start | none | Asha 10, Ben 5, Cara 0 | Opening balances |
| 1 | Asha pays Ben 4 | Asha 6, Ben 9, Cara 0 | Accepted |
| 2 | Ben pays Cara 6 | Asha 6, Ben 3, Cara 6 | Accepted |
| 3 | Cara pays Asha 8 | Asha 6, Ben 3, Cara 6 | Refused, Cara has only 6 |
| 4 | Ben pays Cara 3 | Asha 6, Ben 0, Cara 9 | Accepted |
The cells students fill in are left blank in this preview.
A double spend uses the same funds twice
Dev holds 10 and sends 8 to Eli. Dev also sends 8 to Fay. Both payments look fine on their own, because each one is less than 10. Together they need 16.
A ledger replays in order. The first payment leaves Dev with 2, so the second is refused. The order decides which one is real.
Try it: replay and swap the order
start = {"Dev": 10, "Eli": 0, "Fay": 0}
payments = [["Dev", "Eli", 8], ["Dev", "Fay", 8], ["Eli", "Fay", 3]]
money = {}
for name in start:
money[name] = start[name]
for p in payments:
if money[p[0]] >= p[2]:
money[p[0]] = money[p[0]] - p[2]
money[p[1]] = money[p[1]] + p[2]
print("accepted:", p)
else:
print("refused:", p)
print(money)
# Swap the first two payments and run again.
A common mistake
Warning
Many people think a valid chain stops an overspend by itself. It does not. A chain only shows that no block was changed. A refusal needs a ledger rule that checks the sender's balance before it accepts a payment. Keep the two jobs apart: was it changed, and is it allowed.
The chain gives everybody one agreed order
The second payment is refused only if everybody replays the two in the same order. If one computer saw Fay's payment first, it would refuse Eli's instead.
A chain fixes the order. A payment that is in an earlier block counts first. This is why an agreed order is the real problem a shared ledger solves.