Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Atomic transactions: all or nothing (Gray 1981)#
Jim Gray defined the transaction: a group of actions that is atomic (either all happen or none), consistent, and durable. A bank transfer that debits one account must also credit the other, or do neither, however it fails halfway. Ethereum gives every call this property: if execution fails, every storage write it made is discarded.
The trace shows the debit already written to the working storage before the transfer fails. The committed storage is unchanged all the same: the failure discarded the half-done write. A successful transfer conserves the total.
import matplotlib.pyplot as plt
import blockchainkit as bk
from blockchainkit.vm.visualizers import plot_execution_trace
A transfer from slot 0 to slot 1#
transfer = bk.vm.assemble("""
# stack: amount
LOAD 0 # amount old
OVER
OVER # amount old amount old
SWAP
SUB # amount old old-amount
STORE 0 # debit first (wraps if the balance is too small)
OVER # amount old amount
LT # was the balance too small? (old < amount)
JZ credit
REVERT
credit:
LOAD 1
ADD
STORE 1
""")
accounts = {0: 100, 1: 20}
ok = bk.vm.execute(transfer, arguments=(30,), storage=accounts)
assert dict(ok.storage) == {0: 70, 1: 50} and sum(ok.storage.values()) == 120
Failing halfway#
try:
bk.vm.execute(transfer, arguments=(150,), storage=accounts, trace=True)
except bk.vm.VMError as error:
failed = error.trace
print("reverted after", len(failed), "steps; working storage was", failed[-1].storage)
debit = next(step for step in failed if step.opcode == "STORE")
assert debit.storage[0] == (100 - 150) % 2**256 # The debit was written...
assert accounts == {0: 100, 1: 20} # ...and never committed.
fig, ax = plt.subplots(figsize=(9, 4.5))
plot_execution_trace(failed, ax=ax)
fig.tight_layout()
plt.show()

reverted after 9 steps; working storage was {0: 115792089237316195423570985008687907853269984665640564039457584007913129639886, 1: 20}
Exercise#
Move the balance check before the debit. Does the committed outcome change for any amount? What does atomicity let a contract get away with here, and why is the order of checks and effects still crucial once a contract calls out to another one (see the DAO example)?
A worked solution is in Exercises: execution.
Total running time of the script: (0 minutes 0.119 seconds)