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.
Files changed (101) hide show
  1. blockchainkit/__init__.py +12 -0
  2. blockchainkit/_validation.py +44 -0
  3. blockchainkit/consensus/__init__.py +66 -0
  4. blockchainkit/consensus/core/__init__.py +25 -0
  5. blockchainkit/consensus/core/base.py +97 -0
  6. blockchainkit/consensus/systems/__init__.py +46 -0
  7. blockchainkit/consensus/systems/byzantine.py +91 -0
  8. blockchainkit/consensus/systems/catch_up.py +60 -0
  9. blockchainkit/consensus/systems/difficulty.py +74 -0
  10. blockchainkit/consensus/systems/finality.py +92 -0
  11. blockchainkit/consensus/systems/fork_choice.py +44 -0
  12. blockchainkit/consensus/systems/pbft.py +78 -0
  13. blockchainkit/consensus/systems/pos.py +53 -0
  14. blockchainkit/consensus/systems/pow.py +58 -0
  15. blockchainkit/consensus/systems/pricing.py +50 -0
  16. blockchainkit/consensus/systems/randomized.py +114 -0
  17. blockchainkit/consensus/systems/selfish.py +81 -0
  18. blockchainkit/consensus/systems/sortition.py +48 -0
  19. blockchainkit/consensus/systems/stake_games.py +44 -0
  20. blockchainkit/consensus/systems/synchrony.py +49 -0
  21. blockchainkit/consensus/visualizers/__init__.py +9 -0
  22. blockchainkit/consensus/visualizers/plots.py +80 -0
  23. blockchainkit/constants.py +66 -0
  24. blockchainkit/crypto/__init__.py +157 -0
  25. blockchainkit/crypto/core/__init__.py +21 -0
  26. blockchainkit/crypto/core/base.py +109 -0
  27. blockchainkit/crypto/systems/__init__.py +137 -0
  28. blockchainkit/crypto/systems/asymmetric.py +136 -0
  29. blockchainkit/crypto/systems/commitments.py +93 -0
  30. blockchainkit/crypto/systems/curves.py +205 -0
  31. blockchainkit/crypto/systems/discrete_log.py +156 -0
  32. blockchainkit/crypto/systems/hashing.py +95 -0
  33. blockchainkit/crypto/systems/lamport.py +65 -0
  34. blockchainkit/crypto/systems/mac.py +34 -0
  35. blockchainkit/crypto/systems/merkle_damgard.py +139 -0
  36. blockchainkit/crypto/systems/multisig.py +98 -0
  37. blockchainkit/crypto/systems/one_time_pad.py +42 -0
  38. blockchainkit/crypto/systems/puzzles.py +96 -0
  39. blockchainkit/crypto/systems/sharing.py +159 -0
  40. blockchainkit/crypto/systems/signatures.py +232 -0
  41. blockchainkit/crypto/utils/__init__.py +5 -0
  42. blockchainkit/crypto/utils/primes.py +39 -0
  43. blockchainkit/crypto/visualizers/__init__.py +9 -0
  44. blockchainkit/crypto/visualizers/plots.py +88 -0
  45. blockchainkit/network/__init__.py +70 -0
  46. blockchainkit/network/core/__init__.py +21 -0
  47. blockchainkit/network/core/base.py +149 -0
  48. blockchainkit/network/systems/__init__.py +54 -0
  49. blockchainkit/network/systems/addresses.py +126 -0
  50. blockchainkit/network/systems/broadcast.py +124 -0
  51. blockchainkit/network/systems/clocks.py +149 -0
  52. blockchainkit/network/systems/epidemics.py +111 -0
  53. blockchainkit/network/systems/gossip.py +201 -0
  54. blockchainkit/network/systems/kademlia.py +147 -0
  55. blockchainkit/network/systems/privacy.py +98 -0
  56. blockchainkit/network/systems/propagation.py +60 -0
  57. blockchainkit/network/systems/relay.py +123 -0
  58. blockchainkit/network/systems/replication.py +122 -0
  59. blockchainkit/network/systems/topology.py +238 -0
  60. blockchainkit/network/visualizers/__init__.py +14 -0
  61. blockchainkit/network/visualizers/plots.py +177 -0
  62. blockchainkit/py.typed +0 -0
  63. blockchainkit/structures/__init__.py +61 -0
  64. blockchainkit/structures/core/__init__.py +21 -0
  65. blockchainkit/structures/core/base.py +100 -0
  66. blockchainkit/structures/systems/__init__.py +45 -0
  67. blockchainkit/structures/systems/block.py +144 -0
  68. blockchainkit/structures/systems/bloom.py +81 -0
  69. blockchainkit/structures/systems/chain.py +131 -0
  70. blockchainkit/structures/systems/hash_chain.py +35 -0
  71. blockchainkit/structures/systems/headers.py +54 -0
  72. blockchainkit/structures/systems/ledger.py +87 -0
  73. blockchainkit/structures/systems/merkle.py +273 -0
  74. blockchainkit/structures/systems/mmr.py +112 -0
  75. blockchainkit/structures/systems/sparse_merkle.py +139 -0
  76. blockchainkit/structures/systems/transaction.py +116 -0
  77. blockchainkit/structures/systems/utxo.py +156 -0
  78. blockchainkit/structures/utils/__init__.py +6 -0
  79. blockchainkit/structures/utils/accounts.py +13 -0
  80. blockchainkit/structures/utils/encoding.py +11 -0
  81. blockchainkit/structures/visualizers/__init__.py +13 -0
  82. blockchainkit/structures/visualizers/plots.py +154 -0
  83. blockchainkit/vm/__init__.py +60 -0
  84. blockchainkit/vm/core/__init__.py +25 -0
  85. blockchainkit/vm/core/base.py +171 -0
  86. blockchainkit/vm/systems/__init__.py +40 -0
  87. blockchainkit/vm/systems/assembler.py +80 -0
  88. blockchainkit/vm/systems/expressions.py +91 -0
  89. blockchainkit/vm/systems/programs.py +143 -0
  90. blockchainkit/vm/systems/reentrancy.py +70 -0
  91. blockchainkit/vm/systems/script.py +268 -0
  92. blockchainkit/vm/systems/stack_machine.py +239 -0
  93. blockchainkit/vm/systems/turing.py +111 -0
  94. blockchainkit/vm/systems/verifier.py +81 -0
  95. blockchainkit/vm/visualizers/__init__.py +9 -0
  96. blockchainkit/vm/visualizers/plots.py +97 -0
  97. blockchainkit-0.2.0.dist-info/METADATA +195 -0
  98. blockchainkit-0.2.0.dist-info/RECORD +101 -0
  99. blockchainkit-0.2.0.dist-info/WHEEL +5 -0
  100. blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
  101. 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