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()
State after each instruction
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)

Gallery generated by Sphinx-Gallery