blockchainkit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blockchainkit/__init__.py +12 -0
- blockchainkit/_validation.py +44 -0
- blockchainkit/consensus/__init__.py +66 -0
- blockchainkit/consensus/core/__init__.py +25 -0
- blockchainkit/consensus/core/base.py +97 -0
- blockchainkit/consensus/systems/__init__.py +46 -0
- blockchainkit/consensus/systems/byzantine.py +91 -0
- blockchainkit/consensus/systems/catch_up.py +60 -0
- blockchainkit/consensus/systems/difficulty.py +74 -0
- blockchainkit/consensus/systems/finality.py +92 -0
- blockchainkit/consensus/systems/fork_choice.py +44 -0
- blockchainkit/consensus/systems/pbft.py +78 -0
- blockchainkit/consensus/systems/pos.py +53 -0
- blockchainkit/consensus/systems/pow.py +58 -0
- blockchainkit/consensus/systems/pricing.py +50 -0
- blockchainkit/consensus/systems/randomized.py +114 -0
- blockchainkit/consensus/systems/selfish.py +81 -0
- blockchainkit/consensus/systems/sortition.py +48 -0
- blockchainkit/consensus/systems/stake_games.py +44 -0
- blockchainkit/consensus/systems/synchrony.py +49 -0
- blockchainkit/consensus/visualizers/__init__.py +9 -0
- blockchainkit/consensus/visualizers/plots.py +80 -0
- blockchainkit/constants.py +66 -0
- blockchainkit/crypto/__init__.py +157 -0
- blockchainkit/crypto/core/__init__.py +21 -0
- blockchainkit/crypto/core/base.py +109 -0
- blockchainkit/crypto/systems/__init__.py +137 -0
- blockchainkit/crypto/systems/asymmetric.py +136 -0
- blockchainkit/crypto/systems/commitments.py +93 -0
- blockchainkit/crypto/systems/curves.py +205 -0
- blockchainkit/crypto/systems/discrete_log.py +156 -0
- blockchainkit/crypto/systems/hashing.py +95 -0
- blockchainkit/crypto/systems/lamport.py +65 -0
- blockchainkit/crypto/systems/mac.py +34 -0
- blockchainkit/crypto/systems/merkle_damgard.py +139 -0
- blockchainkit/crypto/systems/multisig.py +98 -0
- blockchainkit/crypto/systems/one_time_pad.py +42 -0
- blockchainkit/crypto/systems/puzzles.py +96 -0
- blockchainkit/crypto/systems/sharing.py +159 -0
- blockchainkit/crypto/systems/signatures.py +232 -0
- blockchainkit/crypto/utils/__init__.py +5 -0
- blockchainkit/crypto/utils/primes.py +39 -0
- blockchainkit/crypto/visualizers/__init__.py +9 -0
- blockchainkit/crypto/visualizers/plots.py +88 -0
- blockchainkit/network/__init__.py +70 -0
- blockchainkit/network/core/__init__.py +21 -0
- blockchainkit/network/core/base.py +149 -0
- blockchainkit/network/systems/__init__.py +54 -0
- blockchainkit/network/systems/addresses.py +126 -0
- blockchainkit/network/systems/broadcast.py +124 -0
- blockchainkit/network/systems/clocks.py +149 -0
- blockchainkit/network/systems/epidemics.py +111 -0
- blockchainkit/network/systems/gossip.py +201 -0
- blockchainkit/network/systems/kademlia.py +147 -0
- blockchainkit/network/systems/privacy.py +98 -0
- blockchainkit/network/systems/propagation.py +60 -0
- blockchainkit/network/systems/relay.py +123 -0
- blockchainkit/network/systems/replication.py +122 -0
- blockchainkit/network/systems/topology.py +238 -0
- blockchainkit/network/visualizers/__init__.py +14 -0
- blockchainkit/network/visualizers/plots.py +177 -0
- blockchainkit/py.typed +0 -0
- blockchainkit/structures/__init__.py +61 -0
- blockchainkit/structures/core/__init__.py +21 -0
- blockchainkit/structures/core/base.py +100 -0
- blockchainkit/structures/systems/__init__.py +45 -0
- blockchainkit/structures/systems/block.py +144 -0
- blockchainkit/structures/systems/bloom.py +81 -0
- blockchainkit/structures/systems/chain.py +131 -0
- blockchainkit/structures/systems/hash_chain.py +35 -0
- blockchainkit/structures/systems/headers.py +54 -0
- blockchainkit/structures/systems/ledger.py +87 -0
- blockchainkit/structures/systems/merkle.py +273 -0
- blockchainkit/structures/systems/mmr.py +112 -0
- blockchainkit/structures/systems/sparse_merkle.py +139 -0
- blockchainkit/structures/systems/transaction.py +116 -0
- blockchainkit/structures/systems/utxo.py +156 -0
- blockchainkit/structures/utils/__init__.py +6 -0
- blockchainkit/structures/utils/accounts.py +13 -0
- blockchainkit/structures/utils/encoding.py +11 -0
- blockchainkit/structures/visualizers/__init__.py +13 -0
- blockchainkit/structures/visualizers/plots.py +154 -0
- blockchainkit/vm/__init__.py +60 -0
- blockchainkit/vm/core/__init__.py +25 -0
- blockchainkit/vm/core/base.py +171 -0
- blockchainkit/vm/systems/__init__.py +40 -0
- blockchainkit/vm/systems/assembler.py +80 -0
- blockchainkit/vm/systems/expressions.py +91 -0
- blockchainkit/vm/systems/programs.py +143 -0
- blockchainkit/vm/systems/reentrancy.py +70 -0
- blockchainkit/vm/systems/script.py +268 -0
- blockchainkit/vm/systems/stack_machine.py +239 -0
- blockchainkit/vm/systems/turing.py +111 -0
- blockchainkit/vm/systems/verifier.py +81 -0
- blockchainkit/vm/visualizers/__init__.py +9 -0
- blockchainkit/vm/visualizers/plots.py +97 -0
- blockchainkit-0.2.0.dist-info/METADATA +195 -0
- blockchainkit-0.2.0.dist-info/RECORD +101 -0
- blockchainkit-0.2.0.dist-info/WHEEL +5 -0
- blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- blockchainkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""A bounded deterministic 256-bit stack machine with atomic storage updates.
|
|
2
|
+
|
|
3
|
+
Instruction encoding: each instruction is a (opcode, operand) pair. Only
|
|
4
|
+
PUSH, LOAD, STORE, JMP, and JZ take an integer operand. All instructions cost
|
|
5
|
+
one teaching gas unit unless a schedule says otherwise; this is not an EVM
|
|
6
|
+
implementation or cost schedule.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
|
|
12
|
+
from blockchainkit._validation import integer
|
|
13
|
+
from blockchainkit.constants import WORD_MODULUS
|
|
14
|
+
from blockchainkit.vm.core.base import ExecutionResult, Instruction, TraceStep, VMError
|
|
15
|
+
|
|
16
|
+
OPERAND_OPCODES = frozenset({"PUSH", "LOAD", "STORE", "JMP", "JZ"})
|
|
17
|
+
"""Opcodes that take a 256-bit integer operand."""
|
|
18
|
+
|
|
19
|
+
STACK_EFFECTS: Mapping[str, tuple[int, int]] = MappingProxyType(
|
|
20
|
+
{
|
|
21
|
+
"PUSH": (0, 1),
|
|
22
|
+
"LOAD": (0, 1),
|
|
23
|
+
"STORE": (1, 0),
|
|
24
|
+
"JMP": (0, 0),
|
|
25
|
+
"JZ": (1, 0),
|
|
26
|
+
"ADD": (2, 1),
|
|
27
|
+
"SUB": (2, 1),
|
|
28
|
+
"MUL": (2, 1),
|
|
29
|
+
"DIV": (2, 1),
|
|
30
|
+
"EQ": (2, 1),
|
|
31
|
+
"LT": (2, 1),
|
|
32
|
+
"DUP": (1, 2),
|
|
33
|
+
"DROP": (1, 0),
|
|
34
|
+
"SWAP": (2, 2),
|
|
35
|
+
"OVER": (2, 3),
|
|
36
|
+
"ROT": (3, 3),
|
|
37
|
+
"STOP": (0, 0),
|
|
38
|
+
"REVERT": (0, 0),
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
"""Values each opcode pops and pushes, as ``(pops, pushes)``.
|
|
42
|
+
|
|
43
|
+
Forth writes these as stack diagrams: ``OVER`` is ``( a b -- a b a )`` and
|
|
44
|
+
``ROT`` is ``( a b c -- b c a )``.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
OPCODES = frozenset(STACK_EFFECTS)
|
|
48
|
+
"""Every opcode the machine accepts."""
|
|
49
|
+
|
|
50
|
+
_ARITHMETIC = frozenset({"ADD", "SUB", "MUL", "DIV", "EQ", "LT"})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_word(value: int, name: str) -> None:
|
|
54
|
+
"""Raise VMError unless ``value`` is an int in ``[0, 2**256)``."""
|
|
55
|
+
if type(value) is not int or not 0 <= value < WORD_MODULUS:
|
|
56
|
+
raise VMError(f"{name} must be a 256-bit word")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_program(program: Iterable[Instruction]) -> tuple[Instruction, ...]:
|
|
60
|
+
"""Check every instruction's shape before anything runs; return the program as a tuple.
|
|
61
|
+
|
|
62
|
+
Raises
|
|
63
|
+
------
|
|
64
|
+
VMError
|
|
65
|
+
An unknown opcode, a missing or extra operand, an operand outside
|
|
66
|
+
256 bits, or a jump target outside the program.
|
|
67
|
+
"""
|
|
68
|
+
code = tuple(program)
|
|
69
|
+
for instruction in code:
|
|
70
|
+
if not isinstance(instruction, tuple) or len(instruction) != 2:
|
|
71
|
+
raise VMError("instructions must be (opcode, operand) pairs")
|
|
72
|
+
op, arg = instruction
|
|
73
|
+
if not isinstance(op, str) or op not in OPCODES:
|
|
74
|
+
raise VMError(f"unknown opcode: {op}")
|
|
75
|
+
if op in OPERAND_OPCODES:
|
|
76
|
+
if type(arg) is not int or not 0 <= arg < WORD_MODULUS:
|
|
77
|
+
raise VMError(f"{op} requires a 256-bit integer operand")
|
|
78
|
+
if op in {"JMP", "JZ"} and arg >= len(code):
|
|
79
|
+
raise VMError("jump target outside program")
|
|
80
|
+
elif arg is not None:
|
|
81
|
+
raise VMError(f"{op} takes no operand")
|
|
82
|
+
return code
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def execute(
|
|
86
|
+
program: Iterable[Instruction],
|
|
87
|
+
*,
|
|
88
|
+
storage: Mapping[int, int] | None = None,
|
|
89
|
+
arguments: Sequence[int] = (),
|
|
90
|
+
gas_limit: int = 10_000,
|
|
91
|
+
stack_limit: int = 1024,
|
|
92
|
+
gas_costs: Mapping[str, int] | None = None,
|
|
93
|
+
trace: bool = False,
|
|
94
|
+
) -> ExecutionResult:
|
|
95
|
+
"""Execute a program against a copy of storage, committing only on success.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
program : iterable of tuple
|
|
100
|
+
(opcode, operand) pairs. For binary operations the top value is the
|
|
101
|
+
right operand: PUSH 7; PUSH 2; SUB produces 5.
|
|
102
|
+
storage : mapping, optional
|
|
103
|
+
Initial 256-bit integer keys and values. Never mutated by execution.
|
|
104
|
+
arguments : sequence of int
|
|
105
|
+
Initial stack, bottom first: the call's inputs, like a contract's
|
|
106
|
+
call data.
|
|
107
|
+
gas_limit : int
|
|
108
|
+
Maximum total gas, including STOP.
|
|
109
|
+
stack_limit : int
|
|
110
|
+
Maximum stack depth.
|
|
111
|
+
gas_costs : mapping, optional
|
|
112
|
+
Per-opcode gas prices overriding the default of one unit each, for
|
|
113
|
+
experiments with a cost schedule. Unlisted opcodes cost one unit;
|
|
114
|
+
every price must be at least one, so gas always bounds the run.
|
|
115
|
+
trace : bool
|
|
116
|
+
Record a :class:`~blockchainkit.vm.core.base.TraceStep` after every
|
|
117
|
+
executed instruction, in ``ExecutionResult.trace`` on success or
|
|
118
|
+
``VMError.trace`` on failure.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
ExecutionResult
|
|
123
|
+
Deterministic stack, storage, gas used, and the trace if requested.
|
|
124
|
+
|
|
125
|
+
Raises
|
|
126
|
+
------
|
|
127
|
+
VMError
|
|
128
|
+
Malformed program, stack underflow or overflow, division by zero,
|
|
129
|
+
``REVERT``, or running out of gas. Storage is then left unchanged.
|
|
130
|
+
|
|
131
|
+
Examples
|
|
132
|
+
--------
|
|
133
|
+
>>> from blockchainkit.vm import execute
|
|
134
|
+
>>> execute([("PUSH", 7), ("PUSH", 2), ("SUB", None)]).stack
|
|
135
|
+
(5,)
|
|
136
|
+
>>> [step.stack for step in execute([("PUSH", 7), ("DUP", None)], trace=True).trace]
|
|
137
|
+
[(7,), (7, 7)]
|
|
138
|
+
>>> execute([("MUL", None)], arguments=(6, 7)).stack
|
|
139
|
+
(42,)
|
|
140
|
+
"""
|
|
141
|
+
integer(gas_limit, "gas_limit")
|
|
142
|
+
integer(stack_limit, "stack_limit", 1)
|
|
143
|
+
code = validate_program(program)
|
|
144
|
+
state = dict(storage or {})
|
|
145
|
+
for key, value in state.items():
|
|
146
|
+
if type(key) is not int or type(value) is not int:
|
|
147
|
+
raise VMError("storage keys and values must be integers")
|
|
148
|
+
check_word(key, "storage keys and values")
|
|
149
|
+
check_word(value, "storage keys and values")
|
|
150
|
+
stack = list(arguments)
|
|
151
|
+
for value in stack:
|
|
152
|
+
check_word(value, "every argument")
|
|
153
|
+
if len(stack) > stack_limit:
|
|
154
|
+
raise VMError("stack limit exceeded")
|
|
155
|
+
prices = dict(gas_costs or {})
|
|
156
|
+
for name, price in prices.items():
|
|
157
|
+
if name not in OPCODES:
|
|
158
|
+
raise VMError(f"gas schedule names an unknown opcode: {name}")
|
|
159
|
+
# A zero price would let a loop run forever without exhausting gas.
|
|
160
|
+
integer(price, f"gas cost of {name}", 1)
|
|
161
|
+
steps: list[TraceStep] = []
|
|
162
|
+
try:
|
|
163
|
+
gas_used = _run(
|
|
164
|
+
code, stack, state, prices, gas_limit, stack_limit, steps if trace else None
|
|
165
|
+
)
|
|
166
|
+
except VMError as error:
|
|
167
|
+
error.trace = tuple(steps)
|
|
168
|
+
raise
|
|
169
|
+
return ExecutionResult(tuple(stack), MappingProxyType(state), gas_used, tuple(steps))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _run(
|
|
173
|
+
code: tuple[Instruction, ...],
|
|
174
|
+
stack: list[int],
|
|
175
|
+
state: dict[int, int],
|
|
176
|
+
prices: Mapping[str, int],
|
|
177
|
+
gas_limit: int,
|
|
178
|
+
stack_limit: int,
|
|
179
|
+
steps: list[TraceStep] | None,
|
|
180
|
+
) -> int:
|
|
181
|
+
pc = gas_used = 0
|
|
182
|
+
while pc < len(code):
|
|
183
|
+
start = pc
|
|
184
|
+
op, arg = code[pc]
|
|
185
|
+
price = prices.get(op, 1)
|
|
186
|
+
if gas_used + price > gas_limit:
|
|
187
|
+
raise VMError("out of gas")
|
|
188
|
+
gas_used += price
|
|
189
|
+
pc += 1
|
|
190
|
+
if len(stack) < STACK_EFFECTS[op][0]:
|
|
191
|
+
raise VMError("stack underflow")
|
|
192
|
+
if op == "REVERT":
|
|
193
|
+
raise VMError("reverted")
|
|
194
|
+
if op == "STOP":
|
|
195
|
+
if steps is not None:
|
|
196
|
+
steps.append(TraceStep(start, op, arg, tuple(stack), dict(state), gas_used))
|
|
197
|
+
break
|
|
198
|
+
if op in OPERAND_OPCODES:
|
|
199
|
+
assert arg is not None # Checked for every operand opcode before execution.
|
|
200
|
+
if op == "PUSH":
|
|
201
|
+
stack.append(arg)
|
|
202
|
+
elif op == "LOAD":
|
|
203
|
+
stack.append(state.get(arg, 0))
|
|
204
|
+
elif op == "STORE":
|
|
205
|
+
state[arg] = stack.pop()
|
|
206
|
+
elif op == "JMP" or stack.pop() == 0:
|
|
207
|
+
pc = arg
|
|
208
|
+
elif op in _ARITHMETIC:
|
|
209
|
+
right, left = stack.pop(), stack.pop()
|
|
210
|
+
if op == "ADD":
|
|
211
|
+
value = left + right
|
|
212
|
+
elif op == "SUB":
|
|
213
|
+
value = left - right
|
|
214
|
+
elif op == "MUL":
|
|
215
|
+
value = left * right
|
|
216
|
+
elif op == "DIV":
|
|
217
|
+
if right == 0:
|
|
218
|
+
raise VMError("division by zero")
|
|
219
|
+
value = left // right
|
|
220
|
+
elif op == "EQ":
|
|
221
|
+
value = int(left == right)
|
|
222
|
+
else:
|
|
223
|
+
value = int(left < right)
|
|
224
|
+
stack.append(value % WORD_MODULUS)
|
|
225
|
+
elif op == "DUP":
|
|
226
|
+
stack.append(stack[-1])
|
|
227
|
+
elif op == "DROP":
|
|
228
|
+
stack.pop()
|
|
229
|
+
elif op == "SWAP":
|
|
230
|
+
stack[-1], stack[-2] = stack[-2], stack[-1]
|
|
231
|
+
elif op == "OVER":
|
|
232
|
+
stack.append(stack[-2])
|
|
233
|
+
else: # ROT: ( a b c -- b c a )
|
|
234
|
+
stack.append(stack.pop(-3))
|
|
235
|
+
if len(stack) > stack_limit:
|
|
236
|
+
raise VMError("stack limit exceeded")
|
|
237
|
+
if steps is not None:
|
|
238
|
+
steps.append(TraceStep(start, op, arg, tuple(stack), dict(state), gas_used))
|
|
239
|
+
return gas_used
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Turing machines (1936), the halting problem, and Radó's busy beaver (1962).
|
|
2
|
+
|
|
3
|
+
A Turing machine reads one cell of an unbounded tape, and according to its
|
|
4
|
+
state and the symbol there, writes a symbol, moves left or right, and
|
|
5
|
+
changes state. Turing proved that no program can decide, for every machine,
|
|
6
|
+
whether it halts. Radó turned that into numbers: the *busy beaver* ``S(n)``
|
|
7
|
+
is the most steps any halting ``n``-state, 2-symbol machine takes from a
|
|
8
|
+
blank tape. ``S`` grows faster than any computable function, so no
|
|
9
|
+
fixed budget can separate "still running" from "never halts". Blockchains
|
|
10
|
+
respond the way Radó's search must: they cap the work (gas) and stop there.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import itertools
|
|
14
|
+
from collections.abc import Iterator, Mapping
|
|
15
|
+
|
|
16
|
+
from blockchainkit._validation import integer
|
|
17
|
+
from blockchainkit.vm.core.base import BusyBeaverResult, TuringRun
|
|
18
|
+
|
|
19
|
+
Rule = tuple[int, int, str]
|
|
20
|
+
"""``(write, move, next_state)``: move is -1 (left) or +1 (right); state ``"H"`` halts."""
|
|
21
|
+
|
|
22
|
+
HALT = "H"
|
|
23
|
+
"""str: The halting state."""
|
|
24
|
+
|
|
25
|
+
STATE_NAMES = "ABCDEFG"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_turing_machine(
|
|
29
|
+
rules: Mapping[tuple[str, int], Rule], *, max_steps: int = 1000, start: str = "A"
|
|
30
|
+
) -> TuringRun:
|
|
31
|
+
"""Run a 2-symbol machine from a blank tape for at most ``max_steps`` steps.
|
|
32
|
+
|
|
33
|
+
The transition into the halting state counts as a step, as in Radó's
|
|
34
|
+
definition. If the machine returns to an earlier configuration (same
|
|
35
|
+
state, head position and tape), it provably runs forever. Otherwise,
|
|
36
|
+
when the budget runs out, the outcome is unknown.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
>>> from blockchainkit.vm import run_turing_machine
|
|
41
|
+
>>> champion = {("A", 0): (1, 1, "B"), ("A", 1): (1, -1, "B"),
|
|
42
|
+
... ("B", 0): (1, -1, "A"), ("B", 1): (1, 1, "H")}
|
|
43
|
+
>>> run = run_turing_machine(champion)
|
|
44
|
+
>>> run.outcome, run.steps, run.ones
|
|
45
|
+
('halted', 6, 4)
|
|
46
|
+
"""
|
|
47
|
+
integer(max_steps, "max_steps", 1)
|
|
48
|
+
tape: dict[int, int] = {}
|
|
49
|
+
state, head = start, 0
|
|
50
|
+
seen: set[tuple[str, int, frozenset[int]]] = set()
|
|
51
|
+
for step in range(1, max_steps + 1):
|
|
52
|
+
config = (state, head, frozenset(cell for cell, symbol in tape.items() if symbol))
|
|
53
|
+
if config in seen:
|
|
54
|
+
return TuringRun("looping", step - 1, sum(tape.values()))
|
|
55
|
+
seen.add(config)
|
|
56
|
+
try:
|
|
57
|
+
write, move, state = rules[(state, tape.get(head, 0))]
|
|
58
|
+
except KeyError:
|
|
59
|
+
raise ValueError(f"no rule for state {state!r} reading {tape.get(head, 0)}") from None
|
|
60
|
+
if write not in (0, 1) or move not in (-1, 1):
|
|
61
|
+
raise ValueError("rules write 0 or 1 and move -1 or +1")
|
|
62
|
+
tape[head] = write
|
|
63
|
+
head += move
|
|
64
|
+
if state == HALT:
|
|
65
|
+
return TuringRun("halted", step, sum(tape.values()))
|
|
66
|
+
return TuringRun("unknown", max_steps, sum(tape.values()))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def enumerate_machines(states: int) -> Iterator[dict[tuple[str, int], Rule]]:
|
|
70
|
+
"""Yield every ``states``-state, 2-symbol machine: ``(4 (states + 1)) ** (2 states)`` of them.
|
|
71
|
+
|
|
72
|
+
>>> from blockchainkit.vm import enumerate_machines
|
|
73
|
+
>>> sum(1 for _ in enumerate_machines(1))
|
|
74
|
+
64
|
|
75
|
+
"""
|
|
76
|
+
integer(states, "states", 1)
|
|
77
|
+
if states > len(STATE_NAMES):
|
|
78
|
+
raise ValueError(f"at most {len(STATE_NAMES)} states")
|
|
79
|
+
names = STATE_NAMES[:states]
|
|
80
|
+
keys = [(name, symbol) for name in names for symbol in (0, 1)]
|
|
81
|
+
choices = list(itertools.product((0, 1), (-1, 1), (*names, HALT)))
|
|
82
|
+
for combination in itertools.product(choices, repeat=len(keys)):
|
|
83
|
+
yield dict(zip(keys, combination, strict=True))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def busy_beaver(states: int, *, max_steps: int = 100) -> BusyBeaverResult:
|
|
87
|
+
"""Search every machine with ``states`` states for the longest halting run.
|
|
88
|
+
|
|
89
|
+
Each machine runs for at most ``max_steps`` steps. The answer equals
|
|
90
|
+
``S(states)`` only if every machine classified as unknown really runs
|
|
91
|
+
forever, which this search cannot prove; that gap is the halting problem.
|
|
92
|
+
Feasible here for 1 or 2 states (20,736 machines for 2).
|
|
93
|
+
|
|
94
|
+
Examples
|
|
95
|
+
--------
|
|
96
|
+
>>> from blockchainkit.vm import busy_beaver
|
|
97
|
+
>>> busy_beaver(1).steps
|
|
98
|
+
1
|
|
99
|
+
"""
|
|
100
|
+
best: TuringRun | None = None
|
|
101
|
+
champion: dict[tuple[str, int], Rule] = {}
|
|
102
|
+
counts = {"halted": 0, "looping": 0, "unknown": 0}
|
|
103
|
+
for rules in enumerate_machines(states):
|
|
104
|
+
run = run_turing_machine(rules, max_steps=max_steps)
|
|
105
|
+
counts[run.outcome] += 1
|
|
106
|
+
if run.outcome == "halted" and (
|
|
107
|
+
best is None or (run.steps, run.ones) > (best.steps, best.ones)
|
|
108
|
+
):
|
|
109
|
+
best, champion = run, rules
|
|
110
|
+
assert best is not None # A machine whose first rule halts always exists.
|
|
111
|
+
return BusyBeaverResult(best.steps, best.ones, champion, counts)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Static bytecode verification (Gosling 1995; the Java virtual machine).
|
|
2
|
+
|
|
3
|
+
Java's virtual machine checks downloaded code *before* running it, so the
|
|
4
|
+
interpreter can skip run-time checks. The verifier follows every path
|
|
5
|
+
through the program, tracking how many values are on the stack at each
|
|
6
|
+
instruction, without running anything. It rejects code that could
|
|
7
|
+
underflow the stack on some path, or that reaches the same instruction with
|
|
8
|
+
different stack heights along different paths (as a loop that pushes on
|
|
9
|
+
every turn does). Code that passes has a known maximum stack depth.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
|
|
14
|
+
from blockchainkit._validation import integer
|
|
15
|
+
from blockchainkit.vm.core.base import Instruction, VerificationResult
|
|
16
|
+
from blockchainkit.vm.systems.stack_machine import STACK_EFFECTS, validate_program
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify_bytecode(program: Iterable[Instruction], *, arguments: int = 0) -> VerificationResult:
|
|
20
|
+
"""Check stack safety on every path and compute the maximum stack depth.
|
|
21
|
+
|
|
22
|
+
This is abstract interpretation: the abstract state at an instruction is
|
|
23
|
+
a single number, the stack height on entry. Starting from ``arguments``
|
|
24
|
+
at instruction 0, the verifier propagates heights along every edge
|
|
25
|
+
(both outcomes of ``JZ``) until nothing changes.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
program : iterable of tuple
|
|
30
|
+
Instructions, validated for shape first.
|
|
31
|
+
arguments : int
|
|
32
|
+
Values on the stack when execution starts.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
VerificationResult
|
|
37
|
+
``max_depth`` and the list of problems found; ``ok`` if there are none.
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> from blockchainkit.vm import verify_bytecode
|
|
42
|
+
>>> verify_bytecode([("PUSH", 1), ("ADD", None)]).errors
|
|
43
|
+
('pc 1: ADD needs 2 values but only 1 can be on the stack',)
|
|
44
|
+
>>> verify_bytecode([("PUSH", 1), ("PUSH", 2), ("ADD", None)]).max_depth
|
|
45
|
+
2
|
|
46
|
+
"""
|
|
47
|
+
code = validate_program(program)
|
|
48
|
+
integer(arguments, "arguments")
|
|
49
|
+
heights: dict[int, int] = {0: arguments} if code else {}
|
|
50
|
+
errors: list[str] = []
|
|
51
|
+
pending = [0] if code else []
|
|
52
|
+
max_depth = arguments
|
|
53
|
+
while pending:
|
|
54
|
+
pc = pending.pop()
|
|
55
|
+
height = heights[pc]
|
|
56
|
+
op, arg = code[pc]
|
|
57
|
+
pops, pushes = STACK_EFFECTS[op]
|
|
58
|
+
if height < pops:
|
|
59
|
+
errors.append(
|
|
60
|
+
f"pc {pc}: {op} needs {pops} values but only {height} can be on the stack"
|
|
61
|
+
)
|
|
62
|
+
continue
|
|
63
|
+
after = height - pops + pushes
|
|
64
|
+
max_depth = max(max_depth, after)
|
|
65
|
+
if op in {"STOP", "REVERT"}:
|
|
66
|
+
continue
|
|
67
|
+
successors = [] if op == "JMP" else [pc + 1]
|
|
68
|
+
if op in {"JMP", "JZ"}:
|
|
69
|
+
assert arg is not None # Jumps always carry a validated target.
|
|
70
|
+
successors.append(arg)
|
|
71
|
+
for target in successors:
|
|
72
|
+
if target >= len(code):
|
|
73
|
+
continue # Falling off the end halts normally.
|
|
74
|
+
if target not in heights:
|
|
75
|
+
heights[target] = after
|
|
76
|
+
pending.append(target)
|
|
77
|
+
elif heights[target] != after:
|
|
78
|
+
message = f"pc {target}: reached with stack heights {heights[target]} and {after}"
|
|
79
|
+
if message not in errors:
|
|
80
|
+
errors.append(message)
|
|
81
|
+
return VerificationResult(max_depth, tuple(errors))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Plotting helpers for blockchainkit.vm.
|
|
2
|
+
|
|
3
|
+
Imports matplotlib, so ``import blockchainkit`` does not load this module;
|
|
4
|
+
import it explicitly: ``from blockchainkit.vm.visualizers import ...``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from blockchainkit.vm.visualizers.plots import plot_execution_trace, plot_stack_height
|
|
8
|
+
|
|
9
|
+
__all__ = ["plot_execution_trace", "plot_stack_height"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Plotting helpers for blockchainkit.vm: execution traces and stack heights."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
|
|
10
|
+
from blockchainkit.vm.core.base import TraceStep
|
|
11
|
+
|
|
12
|
+
__all__ = ["plot_execution_trace", "plot_stack_height"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plot_execution_trace(
|
|
16
|
+
trace: Sequence[TraceStep], *, max_rows: int = 30, ax: Axes | None = None
|
|
17
|
+
) -> Axes:
|
|
18
|
+
"""Tabulate the stack, storage, and gas after each executed instruction.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
trace : sequence of TraceStep
|
|
23
|
+
``ExecutionResult.trace`` or ``VMError.trace`` from
|
|
24
|
+
``execute(..., trace=True)``.
|
|
25
|
+
max_rows : int
|
|
26
|
+
Show at most this many steps (the first ones), so loops stay legible.
|
|
27
|
+
ax : matplotlib.axes.Axes, optional
|
|
28
|
+
Axes to draw on; a new figure is created if omitted.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
matplotlib.axes.Axes
|
|
33
|
+
"""
|
|
34
|
+
if not trace:
|
|
35
|
+
raise ValueError("empty trace: pass trace=True to execute")
|
|
36
|
+
shown = list(trace[:max_rows])
|
|
37
|
+
if ax is None:
|
|
38
|
+
_, ax = plt.subplots(figsize=(9, 0.35 * len(shown) + 1.4))
|
|
39
|
+
rows = [
|
|
40
|
+
[
|
|
41
|
+
str(step.pc),
|
|
42
|
+
step.opcode if step.operand is None else f"{step.opcode} {step.operand}",
|
|
43
|
+
str(list(step.stack)),
|
|
44
|
+
str(dict(step.storage)),
|
|
45
|
+
str(step.gas_used),
|
|
46
|
+
]
|
|
47
|
+
for step in shown
|
|
48
|
+
]
|
|
49
|
+
ax.axis("off")
|
|
50
|
+
table = ax.table(
|
|
51
|
+
cellText=rows,
|
|
52
|
+
colLabels=["pc", "instruction", "stack", "storage", "gas"],
|
|
53
|
+
cellLoc="center",
|
|
54
|
+
loc="center",
|
|
55
|
+
)
|
|
56
|
+
table.auto_set_font_size(False)
|
|
57
|
+
table.set_fontsize(9)
|
|
58
|
+
table.scale(1, 1.4)
|
|
59
|
+
hidden = len(trace) - len(shown)
|
|
60
|
+
suffix = f" (first {len(shown)} of {len(trace)} steps)" if hidden else ""
|
|
61
|
+
ax.set_title("State after each instruction" + suffix)
|
|
62
|
+
return ax
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def plot_stack_height(
|
|
66
|
+
trace: Sequence[TraceStep], *, label: str | None = None, ax: Axes | None = None
|
|
67
|
+
) -> Axes:
|
|
68
|
+
"""Plot the stack height after each executed instruction.
|
|
69
|
+
|
|
70
|
+
The peak is the stack space the run needed: what a hardware stack must
|
|
71
|
+
provide, and what static verification bounds in advance.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
trace : sequence of TraceStep
|
|
76
|
+
From ``execute(..., trace=True)``.
|
|
77
|
+
label : str, optional
|
|
78
|
+
Legend label, to compare several runs on one axes.
|
|
79
|
+
ax : matplotlib.axes.Axes, optional
|
|
80
|
+
Axes to draw on; a new figure is created if omitted.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
matplotlib.axes.Axes
|
|
85
|
+
"""
|
|
86
|
+
if not trace:
|
|
87
|
+
raise ValueError("empty trace: pass trace=True to execute")
|
|
88
|
+
if ax is None:
|
|
89
|
+
_, ax = plt.subplots()
|
|
90
|
+
heights = [len(step.stack) for step in trace]
|
|
91
|
+
ax.step(range(1, len(heights) + 1), heights, where="post", label=label)
|
|
92
|
+
ax.set_xlabel("instructions executed")
|
|
93
|
+
ax.set_ylabel("stack height")
|
|
94
|
+
ax.set_title(f"Stack height (peak {max(heights)})")
|
|
95
|
+
if label is not None:
|
|
96
|
+
ax.legend()
|
|
97
|
+
return ax
|