blockchainkit.vm#
Execution: a deterministic stack machine, its languages, contracts, and attacks.
A deterministic 256-bit stack machine and the languages, programs, and models built on it: assembly, reverse Polish notation, bytecode verification, Turing machines, Bitcoin Script, contracts, and attacks.
Every public name below is re-exported by the subpackage: import it as
bk.vm.<name>. The plotting helpers are the exception: import them
explicitly from blockchainkit.vm.visualizers, which loads Matplotlib.
Types and results#
Instruction type, errors, and result containers for blockchainkit.vm.
- blockchainkit.vm.core.base.Instruction#
An
(opcode, operand)pair; the operand isNonefor simple opcodes.
- class blockchainkit.vm.core.base.TraceStep(pc, opcode, operand, stack, storage, gas_used)[source]#
Bases:
objectMachine state just after one executed instruction.
- Variables:
pc (
int) – Index of the instruction that ran.opcode (
str) – Its opcode.stack (
tupleofint) – Stack after the instruction, bottom first.storage (
collections.abc.Mapping) – Working storage after the instruction (committed only on success).gas_used (
int) – Cumulative gas, including this instruction.
- Parameters:
- exception blockchainkit.vm.core.base.VMError[source]#
Bases:
ValueErrorInvalid instruction, stack operation, arithmetic operation, or resource limit.
- Variables:
trace (
tupleofblockchainkit.vm.core.base.TraceStep) – Steps executed before the failure, whenexecute(..., trace=True)was requested; otherwise empty.
- class blockchainkit.vm.core.base.ExecutionResult(stack, storage, gas_used, trace=())[source]#
Bases:
objectFinal stack, read-only storage, consumed gas, and optional step trace.
- Parameters:
- class blockchainkit.vm.core.base.VerificationResult(max_depth, errors)[source]#
Bases:
objectOutcome of static bytecode verification.
- Variables:
- Parameters:
- class blockchainkit.vm.core.base.TuringRun(outcome, steps, ones)[source]#
Bases:
objectHow a Turing machine run ended.
- Variables:
- Parameters:
- class blockchainkit.vm.core.base.BusyBeaverResult(steps, ones, machine, counts)[source]#
Bases:
objectThe longest-running halting machine found by an exhaustive search.
- Variables:
- Parameters:
- class blockchainkit.vm.core.base.ScriptResult(valid, stack, error, operations)[source]#
Bases:
objectOutcome of validating a Bitcoin-style script pair.
Constructions and protocols#
A bounded deterministic 256-bit stack machine with atomic storage updates.
Instruction encoding: each instruction is a (opcode, operand) pair. Only PUSH, LOAD, STORE, JMP, and JZ take an integer operand. All instructions cost one teaching gas unit unless a schedule says otherwise; this is not an EVM implementation or cost schedule.
- blockchainkit.vm.systems.stack_machine.OPERAND_OPCODES = frozenset({'JMP', 'JZ', 'LOAD', 'PUSH', 'STORE'})#
Opcodes that take a 256-bit integer operand.
- blockchainkit.vm.systems.stack_machine.STACK_EFFECTS: Mapping[str, tuple[int, int]] = mappingproxy({'PUSH': (0, 1), 'LOAD': (0, 1), 'STORE': (1, 0), 'JMP': (0, 0), 'JZ': (1, 0), 'ADD': (2, 1), 'SUB': (2, 1), 'MUL': (2, 1), 'DIV': (2, 1), 'EQ': (2, 1), 'LT': (2, 1), 'DUP': (1, 2), 'DROP': (1, 0), 'SWAP': (2, 2), 'OVER': (2, 3), 'ROT': (3, 3), 'STOP': (0, 0), 'REVERT': (0, 0)})#
Values each opcode pops and pushes, as
(pops, pushes).Forth writes these as stack diagrams:
OVERis( a b -- a b a )andROTis( a b c -- b c a ).
- blockchainkit.vm.systems.stack_machine.OPCODES = frozenset({'ADD', 'DIV', 'DROP', 'DUP', 'EQ', 'JMP', 'JZ', 'LOAD', 'LT', 'MUL', 'OVER', 'PUSH', 'REVERT', 'ROT', 'STOP', 'STORE', 'SUB', 'SWAP'})#
Every opcode the machine accepts.
- blockchainkit.vm.systems.stack_machine.check_word(value, name)[source]#
Raise VMError unless
valueis an int in[0, 2**256).
- blockchainkit.vm.systems.stack_machine.validate_program(program)[source]#
Check every instruction’s shape before anything runs; return the program as a tuple.
- blockchainkit.vm.systems.stack_machine.execute(program, *, storage=None, arguments=(), gas_limit=10000, stack_limit=1024, gas_costs=None, trace=False)[source]#
Execute a program against a copy of storage, committing only on success.
- Parameters:
program (
collections.abc.Iterableoftuple) – (opcode, operand) pairs. For binary operations the top value is the right operand: PUSH 7; PUSH 2; SUB produces 5.storage (
collections.abc.Mapping, optional) – Initial 256-bit integer keys and values. Never mutated by execution.arguments (
collections.abc.Sequenceofint) – Initial stack, bottom first: the call’s inputs, like a contract’s call data.gas_limit (
int) – Maximum total gas, including STOP.stack_limit (
int) – Maximum stack depth.gas_costs (
collections.abc.Mapping, optional) – Per-opcode gas prices overriding the default of one unit each, for experiments with a cost schedule. Unlisted opcodes cost one unit; every price must be at least one, so gas always bounds the run.trace (
bool) – Record aTraceStepafter every executed instruction, inExecutionResult.traceon success orVMError.traceon failure.
- Returns:
Deterministic stack, storage, gas used, and the trace if requested.
- Return type:
- Raises:
VMError – Malformed program, stack underflow or overflow, division by zero,
REVERT, or running out of gas. Storage is then left unchanged.
Examples
>>> from blockchainkit.vm import execute >>> execute([("PUSH", 7), ("PUSH", 2), ("SUB", None)]).stack (5,) >>> [step.stack for step in execute([("PUSH", 7), ("DUP", None)], trace=True).trace] [(7,), (7, 7)] >>> execute([("MUL", None)], arguments=(6, 7)).stack (42,)
A two-pass assembler: write programs as text, with labels for jump targets.
Each line holds one instruction, an opcode with an optional operand, or a
label ending in :. # starts a comment. A jump may name a label
instead of an address; the first pass records where each label points, the
second replaces label names by those addresses.
Example:
# Sum 1 + 2 + ... + n, with n in slot 0 and the sum in slot 1.
loop:
LOAD 0
JZ done
LOAD 1
LOAD 0
ADD
STORE 1
LOAD 0
PUSH 1
SUB
STORE 0
JMP loop
done:
STOP
- blockchainkit.vm.systems.assembler.assemble(source)[source]#
Translate assembly text into
(opcode, operand)instructions.Operands are decimal or
0xhexadecimal integers, or labels forJMPandJZ.Examples
>>> from blockchainkit.vm import assemble >>> assemble(''' ... start: ... PUSH 0x10 # sixteen ... JZ start ... ''') (('PUSH', 16), ('JZ', 0))
Reverse Polish notation (Łukasiewicz 1924; Hamblin 1957) and its compilation.
Łukasiewicz wrote logical formulas with the operator first, which needs no
parentheses: + 1 * 2 3. Hamblin saw that the reversed form,
1 2 3 * +, is exactly the order in which a stack machine computes: push
operands, and let each operator replace the top two values by its result.
Dijkstra’s shunting-yard algorithm (1961) converts ordinary infix notation
to that order, which is how expressions become stack-machine code.
- blockchainkit.vm.systems.expressions.to_rpn(expression)[source]#
Convert an infix expression to reverse Polish notation (shunting yard).
Supports non-negative integers,
+ - * /with the usual precedence and left associativity, and parentheses.Examples
>>> from blockchainkit.vm import to_rpn >>> to_rpn("(1 + 2) * 3 - 4") ('1', '2', '+', '3', '*', '4', '-')
- blockchainkit.vm.systems.expressions.compile_expression(expression)[source]#
Compile an infix expression to stack-machine instructions via RPN.
Each number becomes
PUSHand each operator its opcode. Arithmetic is then the machine’s: modulo2**256, with/as floor division.Examples
>>> from blockchainkit.vm import compile_expression, execute >>> execute(compile_expression("(1 + 2) * 3 - 4")).stack (5,)
Static bytecode verification (Gosling 1995; the Java virtual machine).
Java’s virtual machine checks downloaded code before running it, so the interpreter can skip run-time checks. The verifier follows every path through the program, tracking how many values are on the stack at each instruction, without running anything. It rejects code that could underflow the stack on some path, or that reaches the same instruction with different stack heights along different paths (as a loop that pushes on every turn does). Code that passes has a known maximum stack depth.
- blockchainkit.vm.systems.verifier.verify_bytecode(program, *, arguments=0)[source]#
Check stack safety on every path and compute the maximum stack depth.
This is abstract interpretation: the abstract state at an instruction is a single number, the stack height on entry. Starting from
argumentsat instruction 0, the verifier propagates heights along every edge (both outcomes ofJZ) until nothing changes.- Parameters:
program (
collections.abc.Iterableoftuple) – Instructions, validated for shape first.arguments (
int) – Values on the stack when execution starts.
- Returns:
max_depthand the list of problems found;okif there are none.- Return type:
Examples
>>> from blockchainkit.vm import verify_bytecode >>> verify_bytecode([("PUSH", 1), ("ADD", None)]).errors ('pc 1: ADD needs 2 values but only 1 can be on the stack',) >>> verify_bytecode([("PUSH", 1), ("PUSH", 2), ("ADD", None)]).max_depth 2
Turing machines (1936), the halting problem, and Radó’s busy beaver (1962).
A Turing machine reads one cell of an unbounded tape, and according to its
state and the symbol there, writes a symbol, moves left or right, and
changes state. Turing proved that no program can decide, for every machine,
whether it halts. Radó turned that into numbers: the busy beaver S(n)
is the most steps any halting n-state, 2-symbol machine takes from a
blank tape. S grows faster than any computable function, so no
fixed budget can separate “still running” from “never halts”. Blockchains
respond the way Radó’s search must: they cap the work (gas) and stop there.
- blockchainkit.vm.systems.turing.Rule#
move is -1 (left) or +1 (right); state
"H"halts.- Type:
(write, move, next_state)
- blockchainkit.vm.systems.turing.run_turing_machine(rules, *, max_steps=1000, start='A')[source]#
Run a 2-symbol machine from a blank tape for at most
max_stepssteps.The transition into the halting state counts as a step, as in Radó’s definition. If the machine returns to an earlier configuration (same state, head position and tape), it provably runs forever. Otherwise, when the budget runs out, the outcome is unknown.
Examples
>>> from blockchainkit.vm import run_turing_machine >>> champion = {("A", 0): (1, 1, "B"), ("A", 1): (1, -1, "B"), ... ("B", 0): (1, -1, "A"), ("B", 1): (1, 1, "H")} >>> run = run_turing_machine(champion) >>> run.outcome, run.steps, run.ones ('halted', 6, 4)
- blockchainkit.vm.systems.turing.enumerate_machines(states)[source]#
Yield every
states-state, 2-symbol machine:(4 (states + 1)) ** (2 states)of them.>>> from blockchainkit.vm import enumerate_machines >>> sum(1 for _ in enumerate_machines(1)) 64
- blockchainkit.vm.systems.turing.busy_beaver(states, *, max_steps=100)[source]#
Search every machine with
statesstates for the longest halting run.Each machine runs for at most
max_stepssteps. The answer equalsS(states)only if every machine classified as unknown really runs forever, which this search cannot prove; that gap is the halting problem. Feasible here for 1 or 2 states (20,736 machines for 2).Examples
>>> from blockchainkit.vm import busy_beaver >>> busy_beaver(1).steps 1
- Parameters:
- Return type:
Bitcoin-style Script (2009): spending conditions as tiny stack programs.
A coin is locked by a locking script; to spend it, one supplies an unlocking script. The unlocking script runs first, then the locking script runs on the stack it left, and the spend is valid if no step fails and the top of the stack is true. Script deliberately has no loops, so a script of n operations runs at most n steps and needs no gas.
Teaching differences from Bitcoin: stack items are byte strings, but numbers
are big-endian; OP_HASH256 (double SHA-256) replaces OP_HASH160 in
pay-to-public-key-hash; and OP_CHECKSIG checks a blockchainkit Schnorr
signature over an explicit message rather than a transaction digest.
- blockchainkit.vm.systems.script.ScriptItem = bytes | str#
A data push (bytes) or an opcode name such as
"OP_DUP"(str).
- blockchainkit.vm.systems.script.OPCODES = frozenset({'OP_CHECKLOCKTIMEVERIFY', 'OP_CHECKSIG', 'OP_DROP', 'OP_DUP', 'OP_ELSE', 'OP_ENDIF', 'OP_EQUAL', 'OP_EQUALVERIFY', 'OP_FALSE', 'OP_HASH256', 'OP_IF', 'OP_RETURN', 'OP_SHA256', 'OP_TRUE', 'OP_VERIFY'})#
Every opcode the interpreter accepts.
- blockchainkit.vm.systems.script.encode_public_key(public)[source]#
A public key as stack bytes (uncompressed point encoding).
- blockchainkit.vm.systems.script.encode_signature(signature)[source]#
A signature as stack bytes: the commitment point, then the 32-byte response.
- Parameters:
signature (SchnorrSignature)
- Return type:
- blockchainkit.vm.systems.script.number(value)[source]#
Encode a non-negative integer as minimal big-endian bytes (0 is empty).
- blockchainkit.vm.systems.script.verify_script(unlocking, locking, *, message=b'', locktime=0)[source]#
Run the unlocking script, then the locking script on its stack, and judge the spend.
- Parameters:
unlocking (
collections.abc.Sequenceofbytesorstr) – Data pushes and opcode names.locking (
collections.abc.Sequenceofbytesorstr) – Data pushes and opcode names.message (
bytes) – WhatOP_CHECKSIGsignatures must sign (the spending transaction).locktime (
int) – The spending transaction’s lock time, compared byOP_CHECKLOCKTIMEVERIFY.
- Return type:
Notes
Running the scripts separately matters: until 2010 Bitcoin concatenated them, and the unlocking script
OP_TRUE OP_RETURNended execution early with true on top, spending any coin (CVE-2010-5141).Examples
>>> from blockchainkit.crypto import sha256 >>> from blockchainkit.vm import verify_script >>> verify_script([b"secret"], ["OP_SHA256", sha256(b"secret"), "OP_EQUAL"]).valid True
- blockchainkit.vm.systems.script.p2pkh_locking(public)[source]#
Pay to public-key hash:
OP_DUP OP_HASH256 <hash> OP_EQUALVERIFY OP_CHECKSIG.The coin names only the hash of a key; the spender reveals the key and a signature.
- blockchainkit.vm.systems.script.p2pkh_unlocking(signature, public)[source]#
The matching unlocking script:
<signature> <public key>.
- blockchainkit.vm.systems.script.htlc_locking(payment_hash, recipient, refund, timeout)[source]#
A hash time-locked contract.
The recipient can claim with a signature and the preimage of
payment_hash(SHA-256); aftertimeout, the sender can take a refund with its own signature:OP_IF OP_SHA256 <payment_hash> OP_EQUALVERIFY <recipient> OP_ELSE <timeout> OP_CHECKLOCKTIMEVERIFY OP_DROP <refund> OP_ENDIF OP_CHECKSIG
Small contracts written for the stack machine.
vending_machine(): Szabo’s (1994) example of a contract enforced by a mechanism rather than a court: pay at least the price and an item and your change come out; otherwise nothing happens.batch_transfer(): the token function behind the 2018 BeautyChain (BEC) exploit, wherecount * valueoverflowed 256 bits.
Both are assembled with assemble()
and run with execute().
- blockchainkit.vm.systems.programs.STOCK = 0#
Storage slots of the vending machine.
- blockchainkit.vm.systems.programs.PRICE = 1#
Storage slots of the vending machine.
- blockchainkit.vm.systems.programs.REVENUE = 2#
Storage slots of the vending machine.
- blockchainkit.vm.systems.programs.vending_machine()[source]#
A vending machine: argument
payment; storage holds stock, price and revenue.On success the stock drops by one, the revenue grows by the price, and the change
payment - priceis left on the stack. Paying too little, or buying from an empty machine, reverts, so neither side can be cheated.Examples
>>> from blockchainkit.vm import execute, vending_machine >>> result = execute(vending_machine(), arguments=(5,), storage={0: 3, 1: 2}) >>> result.stack, dict(result.storage) ((3,), {0: 2, 1: 2, 2: 2})
- blockchainkit.vm.systems.programs.batch_transfer(sender, recipients, *, checked=False)[source]#
Pay
value(the one argument) fromsenderto each recipient.Balances live in storage, one slot per account. Like BeautyChain’s
batchTransfer, the program computesamount = count * value, checks the sender’s balance againstamount, debitsamountonce and creditsvalueto every recipient. The multiplication wraps modulo2**256: with two recipients andvalue = 2**255,amountis 0, every check passes, and each recipient receives2**255tokens from nothing.checked=Trueadds the SafeMath testamount / count == value, which rejects the overflow.Examples
>>> from blockchainkit.vm import batch_transfer, execute >>> program = batch_transfer(1, [2, 3]) >>> dict(execute(program, arguments=(10,), storage={1: 100}).storage) {1: 80, 2: 10, 3: 10}
Reentrancy: the DAO attack (2016).
The DAO’s withdrawal code sent ether before zeroing the caller’s balance.
Sending ether to a contract runs that contract’s code, so the attacker’s
contract called withdraw again from inside the payment, while its
balance still showed the full deposit, and again, until the funds ran out
or the call stack’s depth limit stopped it. About 3.6 million ether were
drained in June 2016.
The fix is the checks-effects-interactions order: check the conditions, update the contract’s own state, and only then call out. A re-entrant call then finds a zero balance and gets nothing.
This module models the two orders directly in Python: the bank’s state is two numbers, and “sending” calls the attacker’s fallback.
- blockchainkit.vm.systems.reentrancy.drain_bank(other_deposits, attacker_deposit, *, checks_effects_interactions=False, max_depth=1024)[source]#
Let an attacker that re-enters on every payment withdraw from a bank.
- Parameters:
other_deposits (
int) – Funds belonging to everyone else.attacker_deposit (
int) – The attacker’s own deposit, at least 1.checks_effects_interactions (
bool) – False: the vulnerable order (pay, then zero the balance). True: zero the balance before paying.max_depth (
int) – Maximum call depth (1024 in Ethereum then), bounding the recursion.
- Return type:
Examples
>>> from blockchainkit.vm import drain_bank >>> drain_bank(90, 10).stolen, drain_bank(90, 10, checks_effects_interactions=True).stolen (90, 0)
Plotting#
Plotting helpers for blockchainkit.vm: execution traces and stack heights.
- blockchainkit.vm.visualizers.plots.plot_execution_trace(trace, *, max_rows=30, ax=None)[source]#
Tabulate the stack, storage, and gas after each executed instruction.
- Parameters:
trace (
collections.abc.Sequenceofblockchainkit.vm.core.base.TraceStep) –ExecutionResult.traceorVMError.tracefromexecute(..., trace=True).max_rows (
int) – Show at most this many steps (the first ones), so loops stay legible.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.vm.visualizers.plots.plot_stack_height(trace, *, label=None, ax=None)[source]#
Plot the stack height after each executed instruction.
The peak is the stack space the run needed: what a hardware stack must provide, and what static verification bounds in advance.
- Parameters:
trace (
collections.abc.Sequenceofblockchainkit.vm.core.base.TraceStep) – Fromexecute(..., trace=True).label (
str, optional) – Legend label, to compare several runs on one axes.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: