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,156 @@
|
|
|
1
|
+
"""The unspent-transaction-output (UTXO) model of Bitcoin (2008).
|
|
2
|
+
|
|
3
|
+
There are no accounts. Value lives in *coins*, outputs of earlier
|
|
4
|
+
transactions, each owned by an address. A transaction consumes whole coins
|
|
5
|
+
as inputs and creates new ones as outputs; any value not assigned to an
|
|
6
|
+
output is the miner's fee. A coin can be spent once: double spending is
|
|
7
|
+
the attempt to use one input twice.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from dataclasses import dataclass, field, replace
|
|
12
|
+
|
|
13
|
+
from blockchainkit._validation import integer
|
|
14
|
+
from blockchainkit.constants import UINT64_LIMIT, UTXO_DOMAIN
|
|
15
|
+
from blockchainkit.crypto.core.base import SchnorrSignature
|
|
16
|
+
from blockchainkit.crypto.systems.curves import public_key
|
|
17
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
18
|
+
from blockchainkit.crypto.systems.signatures import deterministic_nonce, sign, verify
|
|
19
|
+
from blockchainkit.structures.core.base import Coin, OutPoint
|
|
20
|
+
from blockchainkit.structures.systems.transaction import address
|
|
21
|
+
from blockchainkit.structures.utils.accounts import is_account_id
|
|
22
|
+
from blockchainkit.structures.utils.encoding import canonical_json
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class UTXOTransaction:
|
|
27
|
+
"""Spend whole coins and create new ones.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
inputs : tuple of OutPoint
|
|
32
|
+
Coins to consume, each at most once.
|
|
33
|
+
outputs : tuple
|
|
34
|
+
``(owner_address, amount)`` pairs; amounts are positive integers.
|
|
35
|
+
witnesses : tuple
|
|
36
|
+
One ``(public_key, SchnorrSignature)`` per input, added by
|
|
37
|
+
:meth:`signed`.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
inputs: tuple[OutPoint, ...]
|
|
41
|
+
outputs: tuple[tuple[str, int], ...]
|
|
42
|
+
witnesses: tuple[tuple[tuple[int, int], SchnorrSignature], ...] = field(default=())
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if not self.inputs or not self.outputs:
|
|
46
|
+
raise ValueError("a transaction needs inputs and outputs")
|
|
47
|
+
if len(set(self.inputs)) != len(self.inputs):
|
|
48
|
+
raise ValueError("an input cannot be listed twice")
|
|
49
|
+
for owner, amount in self.outputs:
|
|
50
|
+
if not is_account_id(owner):
|
|
51
|
+
raise ValueError("output owners must be account identifiers")
|
|
52
|
+
integer(amount, "amount", 1)
|
|
53
|
+
if amount >= UINT64_LIMIT:
|
|
54
|
+
raise ValueError("amounts must fit unsigned 64-bit integers")
|
|
55
|
+
|
|
56
|
+
def payload(self) -> bytes:
|
|
57
|
+
"""Return the bytes each input's owner signs."""
|
|
58
|
+
return canonical_json(
|
|
59
|
+
{
|
|
60
|
+
"domain": UTXO_DOMAIN,
|
|
61
|
+
"inputs": [[i.txid.hex(), i.index] for i in self.inputs],
|
|
62
|
+
"outputs": [list(o) for o in self.outputs],
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def txid(self) -> bytes:
|
|
68
|
+
"""Return the hash of the unsigned payload; outputs are named by it."""
|
|
69
|
+
return sha256(self.payload())
|
|
70
|
+
|
|
71
|
+
def signed(self, privates: Sequence[int]) -> "UTXOTransaction":
|
|
72
|
+
"""Return a copy with one signature per input, keys given in input order.
|
|
73
|
+
|
|
74
|
+
Each witness is the signer's public key and a Schnorr signature over
|
|
75
|
+
:meth:`payload` with an RFC 6979 nonce. Whether the key owns the coin
|
|
76
|
+
is checked by :meth:`UTXOSet.apply`.
|
|
77
|
+
"""
|
|
78
|
+
if len(privates) != len(self.inputs):
|
|
79
|
+
raise ValueError("give exactly one private key per input")
|
|
80
|
+
payload = self.payload()
|
|
81
|
+
witnesses = tuple(
|
|
82
|
+
(public_key(x), sign(payload, x, nonce=deterministic_nonce(x, payload)))
|
|
83
|
+
for x in privates
|
|
84
|
+
)
|
|
85
|
+
return replace(self, witnesses=witnesses)
|
|
86
|
+
|
|
87
|
+
def fee(self, utxos: "UTXOSet") -> int:
|
|
88
|
+
"""Return inputs minus outputs: what the miner may claim."""
|
|
89
|
+
return sum(utxos[i].amount for i in self.inputs) - sum(a for _, a in self.outputs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class UTXOSet:
|
|
93
|
+
"""An immutable set of unspent coins; applying a transaction returns a new set.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
>>> import blockchainkit as bk
|
|
98
|
+
>>> alice = bk.structures.address(bk.crypto.public_key(7))
|
|
99
|
+
>>> bk.structures.UTXOSet.genesis({alice: 50}).balance(alice)
|
|
100
|
+
50
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, coins: Mapping[OutPoint, Coin]) -> None:
|
|
104
|
+
self._coins = dict(coins)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def genesis(cls, allocations: Mapping[str, int]) -> "UTXOSet":
|
|
108
|
+
"""Create one coin per address from an initial allocation."""
|
|
109
|
+
coins = {}
|
|
110
|
+
for index, (owner, amount) in enumerate(sorted(allocations.items())):
|
|
111
|
+
coins[OutPoint(sha256(b"genesis"), index)] = Coin(owner, amount)
|
|
112
|
+
return cls(coins)
|
|
113
|
+
|
|
114
|
+
def __getitem__(self, outpoint: OutPoint) -> Coin:
|
|
115
|
+
return self._coins[outpoint]
|
|
116
|
+
|
|
117
|
+
def __contains__(self, outpoint: object) -> bool:
|
|
118
|
+
return outpoint in self._coins
|
|
119
|
+
|
|
120
|
+
def __len__(self) -> int:
|
|
121
|
+
return len(self._coins)
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return f"UTXOSet(coins={len(self)}, total={sum(c.amount for c in self._coins.values())})"
|
|
125
|
+
|
|
126
|
+
def coins_of(self, owner: str) -> tuple[OutPoint, ...]:
|
|
127
|
+
"""Return the outpoints of every coin an address owns, sorted."""
|
|
128
|
+
return tuple(sorted(o for o, coin in self._coins.items() if coin.owner == owner))
|
|
129
|
+
|
|
130
|
+
def balance(self, owner: str) -> int:
|
|
131
|
+
"""Return the total value of an address's coins (a wallet's view)."""
|
|
132
|
+
return sum(self._coins[o].amount for o in self.coins_of(owner))
|
|
133
|
+
|
|
134
|
+
def apply(self, tx: UTXOTransaction) -> "UTXOSet":
|
|
135
|
+
"""Validate a transaction and return the set after it.
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
ValueError
|
|
140
|
+
An input is spent or unknown, a witness is missing or does not
|
|
141
|
+
match the coin's owner, or outputs exceed inputs.
|
|
142
|
+
"""
|
|
143
|
+
if len(tx.witnesses) != len(tx.inputs):
|
|
144
|
+
raise ValueError("every input needs a signature")
|
|
145
|
+
for outpoint, (key, signature) in zip(tx.inputs, tx.witnesses, strict=True):
|
|
146
|
+
if outpoint not in self._coins:
|
|
147
|
+
raise ValueError(f"{outpoint} is not an unspent output")
|
|
148
|
+
owner = self._coins[outpoint].owner
|
|
149
|
+
if address(key) != owner or not verify(tx.payload(), signature, key):
|
|
150
|
+
raise ValueError(f"bad signature for {outpoint}")
|
|
151
|
+
if tx.fee(self) < 0:
|
|
152
|
+
raise ValueError("outputs exceed inputs")
|
|
153
|
+
coins = {o: c for o, c in self._coins.items() if o not in tx.inputs}
|
|
154
|
+
for index, (owner, amount) in enumerate(tx.outputs):
|
|
155
|
+
coins[OutPoint(tx.txid, index)] = Coin(owner, amount)
|
|
156
|
+
return UTXOSet(coins)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Encoding and identifier helpers that support the authenticated data structures."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.structures.utils.accounts import is_account_id
|
|
4
|
+
from blockchainkit.structures.utils.encoding import canonical_json
|
|
5
|
+
|
|
6
|
+
__all__ = ["canonical_json", "is_account_id"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Account identifiers: 64 lowercase hex characters, the SHA-256 of a public key."""
|
|
2
|
+
|
|
3
|
+
_HEX = frozenset("0123456789abcdef")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_account_id(value: object) -> bool:
|
|
7
|
+
"""Return whether ``value`` has the shape of an account identifier.
|
|
8
|
+
|
|
9
|
+
>>> from blockchainkit.structures.utils import is_account_id
|
|
10
|
+
>>> is_account_id("ab" * 32), is_account_id("AB" * 32)
|
|
11
|
+
(True, False)
|
|
12
|
+
"""
|
|
13
|
+
return isinstance(value, str) and len(value) == 64 and _HEX.issuperset(value)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Deterministic byte encodings for hashing and signing records."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def canonical_json(value: object) -> bytes:
|
|
7
|
+
"""Encode internal integer/string records as sorted compact UTF-8 JSON.
|
|
8
|
+
|
|
9
|
+
This is a package-specific encoding, not a general canonical-JSON standard.
|
|
10
|
+
"""
|
|
11
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Plotting helpers for blockchainkit.structures.
|
|
2
|
+
|
|
3
|
+
Imports matplotlib, so ``import blockchainkit`` does not load this module;
|
|
4
|
+
import it explicitly: ``from blockchainkit.structures.visualizers import ...``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from blockchainkit.structures.visualizers.plots import (
|
|
8
|
+
plot_block_tree,
|
|
9
|
+
plot_merkle_tree,
|
|
10
|
+
plot_proof_trace,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ["plot_merkle_tree", "plot_proof_trace", "plot_block_tree"]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Plotting helpers for blockchainkit.structures: Merkle trees, proof traces, block trees."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
from matplotlib.axes import Axes
|
|
7
|
+
|
|
8
|
+
from blockchainkit.structures.core.base import MerkleTrace
|
|
9
|
+
from blockchainkit.structures.systems.chain import Blockchain
|
|
10
|
+
from blockchainkit.structures.systems.merkle import MerkleTree
|
|
11
|
+
|
|
12
|
+
__all__ = ["plot_merkle_tree", "plot_proof_trace", "plot_block_tree"]
|
|
13
|
+
|
|
14
|
+
_PATH, _SIBLING, _OTHER = "#2563eb", "#ea580c", "#cbd5e1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plot_merkle_tree(
|
|
18
|
+
tree: MerkleTree, *, highlight: int | None = None, ax: Axes | None = None
|
|
19
|
+
) -> Axes:
|
|
20
|
+
"""Draw every level of a Merkle tree, optionally highlighting one proof.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
tree : MerkleTree
|
|
25
|
+
A tree with at least one leaf.
|
|
26
|
+
highlight : int, optional
|
|
27
|
+
Leaf index whose authentication path (blue) and proof siblings
|
|
28
|
+
(orange) are colored: the siblings are exactly what a proof sends.
|
|
29
|
+
ax : matplotlib.axes.Axes, optional
|
|
30
|
+
Axes to draw on; a new figure is created if omitted.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
matplotlib.axes.Axes
|
|
35
|
+
"""
|
|
36
|
+
if tree.leaf_count == 0:
|
|
37
|
+
raise ValueError("cannot draw an empty tree")
|
|
38
|
+
if ax is None:
|
|
39
|
+
_, ax = plt.subplots(figsize=(max(6, 1.4 * tree.leaf_count), 1.3 * len(tree.levels) + 1))
|
|
40
|
+
width = len(tree.levels[0])
|
|
41
|
+
positions: list[list[float]] = [[i + 0.5 for i in range(width)]]
|
|
42
|
+
for level in tree.levels[1:]:
|
|
43
|
+
below = positions[-1]
|
|
44
|
+
positions.append(
|
|
45
|
+
[(below[2 * i] + below[min(2 * i + 1, len(below) - 1)]) / 2 for i in range(len(level))]
|
|
46
|
+
)
|
|
47
|
+
path = {}
|
|
48
|
+
if highlight is not None:
|
|
49
|
+
tree.proof(highlight) # Validates the index.
|
|
50
|
+
position = highlight
|
|
51
|
+
for depth in range(len(tree.levels)):
|
|
52
|
+
path[(depth, position)] = _PATH
|
|
53
|
+
if (position ^ 1) < len(tree.levels[depth]):
|
|
54
|
+
path[(depth, position ^ 1)] = _SIBLING
|
|
55
|
+
position //= 2
|
|
56
|
+
for depth, level in enumerate(tree.levels):
|
|
57
|
+
for i, digest in enumerate(level):
|
|
58
|
+
x, y = positions[depth][i], depth
|
|
59
|
+
if depth + 1 < len(tree.levels):
|
|
60
|
+
ax.plot([x, positions[depth + 1][i // 2]], [y, y + 1], color="#94a3b8", zorder=1)
|
|
61
|
+
color = path.get((depth, i), _OTHER)
|
|
62
|
+
ink = "black" if color == _OTHER else "white"
|
|
63
|
+
ax.scatter([x], [y], s=900, color=color, zorder=2)
|
|
64
|
+
ax.text(
|
|
65
|
+
x, y, digest.hex()[:6], ha="center", va="center", fontsize=7, color=ink, zorder=3
|
|
66
|
+
)
|
|
67
|
+
top_x = positions[-1][0]
|
|
68
|
+
note = f"root = H(0x02 || count={tree.leaf_count} || top digest)"
|
|
69
|
+
ax.text(top_x, len(tree.levels) - 0.55, note, ha="center", fontsize=9)
|
|
70
|
+
ax.set_ylim(-0.7, len(tree.levels) - 0.2)
|
|
71
|
+
ax.set_xlim(0, width)
|
|
72
|
+
ax.axis("off")
|
|
73
|
+
title = "Merkle tree (digest prefixes)"
|
|
74
|
+
if highlight is not None:
|
|
75
|
+
title += f": proof for leaf {highlight} sends the orange siblings"
|
|
76
|
+
ax.set_title(title)
|
|
77
|
+
return ax
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def plot_proof_trace(trace: MerkleTrace, ax: Axes | None = None) -> Axes:
|
|
81
|
+
"""Tabulate each step of reconstructing a root from a leaf and its proof.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
trace : MerkleTrace
|
|
86
|
+
From :func:`~blockchainkit.structures.systems.merkle.trace_proof`.
|
|
87
|
+
ax : matplotlib.axes.Axes, optional
|
|
88
|
+
Axes to draw on; a new figure is created if omitted.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
matplotlib.axes.Axes
|
|
93
|
+
"""
|
|
94
|
+
if ax is None:
|
|
95
|
+
_, ax = plt.subplots(figsize=(9, 0.5 * len(trace.steps) + 1.6))
|
|
96
|
+
describe = {
|
|
97
|
+
"left": "hash(sibling || node)",
|
|
98
|
+
"right": "hash(node || sibling)",
|
|
99
|
+
"promoted": "no sibling: promote",
|
|
100
|
+
}
|
|
101
|
+
rows = [["hash(0x00 || leaf)", "", trace.leaf_digest.hex()[:16]]]
|
|
102
|
+
for step in trace.steps:
|
|
103
|
+
sibling = "" if step.sibling is None else step.sibling.hex()[:16]
|
|
104
|
+
rows.append([describe[step.side], sibling, step.digest.hex()[:16]])
|
|
105
|
+
verdict = "matches trusted root" if trace.valid else "does NOT match trusted root"
|
|
106
|
+
rows.append([f"bind leaf count; {verdict}", "", trace.root.hex()[:16]])
|
|
107
|
+
ax.axis("off")
|
|
108
|
+
table = ax.table(
|
|
109
|
+
cellText=rows, colLabels=["step", "sibling", "result"], cellLoc="center", loc="center"
|
|
110
|
+
)
|
|
111
|
+
table.auto_set_font_size(False)
|
|
112
|
+
table.set_fontsize(9)
|
|
113
|
+
table.scale(1, 1.6)
|
|
114
|
+
ax.set_title("A Merkle proof is a recipe for reconstructing the root")
|
|
115
|
+
return ax
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def plot_block_tree(chain: Blockchain, ax: Axes | None = None) -> Axes:
|
|
119
|
+
"""Draw every stored block by height, one lane per fork, canonical chain highlighted.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
chain : Blockchain
|
|
124
|
+
A chain, possibly holding side forks.
|
|
125
|
+
ax : matplotlib.axes.Axes, optional
|
|
126
|
+
Axes to draw on; a new figure is created if omitted.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
matplotlib.axes.Axes
|
|
131
|
+
"""
|
|
132
|
+
if ax is None:
|
|
133
|
+
_, ax = plt.subplots(figsize=(8, 3))
|
|
134
|
+
canonical = {block.hash for block in chain.canonical_blocks()}
|
|
135
|
+
lane: dict[bytes, int] = {}
|
|
136
|
+
for index, tip in enumerate(chain.tips()):
|
|
137
|
+
block = tip
|
|
138
|
+
while block.hash not in lane:
|
|
139
|
+
lane[block.hash] = index
|
|
140
|
+
if block.height == 0:
|
|
141
|
+
break
|
|
142
|
+
block = chain.blocks[block.previous_hash]
|
|
143
|
+
for digest, block in chain.blocks.items():
|
|
144
|
+
x, y = block.height, -lane[digest]
|
|
145
|
+
if block.height:
|
|
146
|
+
parent = chain.blocks[block.previous_hash]
|
|
147
|
+
ax.plot([parent.height, x], [-lane[parent.hash], y], color="#94a3b8", zorder=1)
|
|
148
|
+
color = _PATH if digest in canonical else _OTHER
|
|
149
|
+
ax.scatter([x], [y], s=700, marker="s", color=color, zorder=2)
|
|
150
|
+
ax.text(x, y, digest.hex()[:4], ha="center", va="center", fontsize=7, zorder=3)
|
|
151
|
+
ax.set_xlabel("height")
|
|
152
|
+
ax.set_yticks([])
|
|
153
|
+
ax.set_title(f"Block tree: canonical chain in blue (work {chain.cumulative_work})")
|
|
154
|
+
return ax
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Execution: a deterministic stack machine, its languages, contracts, and attacks."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.vm.core.base import (
|
|
4
|
+
BusyBeaverResult,
|
|
5
|
+
ExecutionResult,
|
|
6
|
+
Instruction,
|
|
7
|
+
ReentrancyResult,
|
|
8
|
+
ScriptResult,
|
|
9
|
+
TraceStep,
|
|
10
|
+
TuringRun,
|
|
11
|
+
VerificationResult,
|
|
12
|
+
VMError,
|
|
13
|
+
)
|
|
14
|
+
from blockchainkit.vm.systems.assembler import assemble
|
|
15
|
+
from blockchainkit.vm.systems.expressions import compile_expression, to_rpn
|
|
16
|
+
from blockchainkit.vm.systems.programs import batch_transfer, vending_machine
|
|
17
|
+
from blockchainkit.vm.systems.reentrancy import drain_bank
|
|
18
|
+
from blockchainkit.vm.systems.script import (
|
|
19
|
+
encode_public_key,
|
|
20
|
+
encode_signature,
|
|
21
|
+
htlc_locking,
|
|
22
|
+
number,
|
|
23
|
+
p2pkh_locking,
|
|
24
|
+
p2pkh_unlocking,
|
|
25
|
+
verify_script,
|
|
26
|
+
)
|
|
27
|
+
from blockchainkit.vm.systems.stack_machine import execute, validate_program
|
|
28
|
+
from blockchainkit.vm.systems.turing import busy_beaver, enumerate_machines, run_turing_machine
|
|
29
|
+
from blockchainkit.vm.systems.verifier import verify_bytecode
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"BusyBeaverResult",
|
|
33
|
+
"ExecutionResult",
|
|
34
|
+
"Instruction",
|
|
35
|
+
"ReentrancyResult",
|
|
36
|
+
"ScriptResult",
|
|
37
|
+
"TraceStep",
|
|
38
|
+
"TuringRun",
|
|
39
|
+
"VerificationResult",
|
|
40
|
+
"VMError",
|
|
41
|
+
"assemble",
|
|
42
|
+
"compile_expression",
|
|
43
|
+
"to_rpn",
|
|
44
|
+
"batch_transfer",
|
|
45
|
+
"vending_machine",
|
|
46
|
+
"drain_bank",
|
|
47
|
+
"encode_public_key",
|
|
48
|
+
"encode_signature",
|
|
49
|
+
"htlc_locking",
|
|
50
|
+
"number",
|
|
51
|
+
"p2pkh_locking",
|
|
52
|
+
"p2pkh_unlocking",
|
|
53
|
+
"verify_script",
|
|
54
|
+
"execute",
|
|
55
|
+
"validate_program",
|
|
56
|
+
"busy_beaver",
|
|
57
|
+
"enumerate_machines",
|
|
58
|
+
"run_turing_machine",
|
|
59
|
+
"verify_bytecode",
|
|
60
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Instruction type, errors, and result containers."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.vm.core.base import (
|
|
4
|
+
BusyBeaverResult,
|
|
5
|
+
ExecutionResult,
|
|
6
|
+
Instruction,
|
|
7
|
+
ReentrancyResult,
|
|
8
|
+
ScriptResult,
|
|
9
|
+
TraceStep,
|
|
10
|
+
TuringRun,
|
|
11
|
+
VerificationResult,
|
|
12
|
+
VMError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BusyBeaverResult",
|
|
17
|
+
"ExecutionResult",
|
|
18
|
+
"Instruction",
|
|
19
|
+
"ReentrancyResult",
|
|
20
|
+
"ScriptResult",
|
|
21
|
+
"TraceStep",
|
|
22
|
+
"TuringRun",
|
|
23
|
+
"VerificationResult",
|
|
24
|
+
"VMError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Instruction type, errors, and result containers for blockchainkit.vm."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
Instruction = tuple[str, int | None]
|
|
7
|
+
"""An ``(opcode, operand)`` pair; the operand is ``None`` for simple opcodes."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class TraceStep:
|
|
12
|
+
"""Machine state just after one executed instruction.
|
|
13
|
+
|
|
14
|
+
Attributes
|
|
15
|
+
----------
|
|
16
|
+
pc : int
|
|
17
|
+
Index of the instruction that ran.
|
|
18
|
+
opcode : str
|
|
19
|
+
Its opcode.
|
|
20
|
+
operand : int or None
|
|
21
|
+
Its operand.
|
|
22
|
+
stack : tuple of int
|
|
23
|
+
Stack after the instruction, bottom first.
|
|
24
|
+
storage : Mapping
|
|
25
|
+
Working storage after the instruction (committed only on success).
|
|
26
|
+
gas_used : int
|
|
27
|
+
Cumulative gas, including this instruction.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
pc: int
|
|
31
|
+
opcode: str
|
|
32
|
+
operand: int | None
|
|
33
|
+
stack: tuple[int, ...]
|
|
34
|
+
storage: Mapping[int, int]
|
|
35
|
+
gas_used: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VMError(ValueError):
|
|
39
|
+
"""Invalid instruction, stack operation, arithmetic operation, or resource limit.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
trace : tuple of TraceStep
|
|
44
|
+
Steps executed before the failure, when ``execute(..., trace=True)``
|
|
45
|
+
was requested; otherwise empty.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
trace: tuple[TraceStep, ...] = ()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ExecutionResult:
|
|
53
|
+
"""Final stack, read-only storage, consumed gas, and optional step trace."""
|
|
54
|
+
|
|
55
|
+
stack: tuple[int, ...]
|
|
56
|
+
storage: Mapping[int, int]
|
|
57
|
+
gas_used: int
|
|
58
|
+
trace: tuple[TraceStep, ...] = ()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class VerificationResult:
|
|
63
|
+
"""Outcome of static bytecode verification.
|
|
64
|
+
|
|
65
|
+
Attributes
|
|
66
|
+
----------
|
|
67
|
+
max_depth : int
|
|
68
|
+
The largest stack height any path can reach.
|
|
69
|
+
errors : tuple of str
|
|
70
|
+
Every problem found; empty if the program is safe.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
max_depth: int
|
|
74
|
+
errors: tuple[str, ...]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def ok(self) -> bool:
|
|
78
|
+
"""True if no path can underflow and every join agrees on the stack height."""
|
|
79
|
+
return not self.errors
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class TuringRun:
|
|
84
|
+
"""How a Turing machine run ended.
|
|
85
|
+
|
|
86
|
+
Attributes
|
|
87
|
+
----------
|
|
88
|
+
outcome : str
|
|
89
|
+
``"halted"``, ``"looping"`` (a configuration repeated, so it never
|
|
90
|
+
halts), or ``"unknown"`` (the step budget ran out first).
|
|
91
|
+
steps : int
|
|
92
|
+
Steps executed, counting the halting transition.
|
|
93
|
+
ones : int
|
|
94
|
+
Number of 1s on the tape at the end.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
outcome: str
|
|
98
|
+
steps: int
|
|
99
|
+
ones: int
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class BusyBeaverResult:
|
|
104
|
+
"""The longest-running halting machine found by an exhaustive search.
|
|
105
|
+
|
|
106
|
+
Attributes
|
|
107
|
+
----------
|
|
108
|
+
steps : int
|
|
109
|
+
Its number of steps: the busy-beaver value S if no unknown machine halts later.
|
|
110
|
+
ones : int
|
|
111
|
+
The 1s it leaves on the tape.
|
|
112
|
+
machine : dict
|
|
113
|
+
Its rules, ``(state, symbol) -> (write, move, next_state)``.
|
|
114
|
+
counts : dict
|
|
115
|
+
Machines per outcome: ``"halted"``, ``"looping"``, ``"unknown"``.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
steps: int
|
|
119
|
+
ones: int
|
|
120
|
+
machine: dict[tuple[str, int], tuple[int, int, str]]
|
|
121
|
+
counts: dict[str, int]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class ScriptResult:
|
|
126
|
+
"""Outcome of validating a Bitcoin-style script pair.
|
|
127
|
+
|
|
128
|
+
Attributes
|
|
129
|
+
----------
|
|
130
|
+
valid : bool
|
|
131
|
+
Both scripts ran without error and left a true value on top.
|
|
132
|
+
stack : tuple of bytes
|
|
133
|
+
The final stack, bottom first.
|
|
134
|
+
error : str or None
|
|
135
|
+
Why validation failed, if it did.
|
|
136
|
+
operations : int
|
|
137
|
+
Opcodes and pushes executed. Scripts have no loops, so this never
|
|
138
|
+
exceeds the combined script length.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
valid: bool
|
|
142
|
+
stack: tuple[bytes, ...]
|
|
143
|
+
error: str | None
|
|
144
|
+
operations: int
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class ReentrancyResult:
|
|
149
|
+
"""What an attacker withdrew from a bank contract.
|
|
150
|
+
|
|
151
|
+
Attributes
|
|
152
|
+
----------
|
|
153
|
+
withdrawn : int
|
|
154
|
+
Total paid out to the attacker.
|
|
155
|
+
deposited : int
|
|
156
|
+
What the attacker had deposited.
|
|
157
|
+
calls : int
|
|
158
|
+
Times ``withdraw`` was entered, including re-entries.
|
|
159
|
+
bank_balance : int
|
|
160
|
+
Funds left in the bank afterwards.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
withdrawn: int
|
|
164
|
+
deposited: int
|
|
165
|
+
calls: int
|
|
166
|
+
bank_balance: int
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def stolen(self) -> int:
|
|
170
|
+
"""Withdrawn beyond the attacker's own deposit."""
|
|
171
|
+
return self.withdrawn - self.deposited
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Execution engines and the programs, languages and models built on them."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.vm.systems.assembler import assemble
|
|
4
|
+
from blockchainkit.vm.systems.expressions import compile_expression, to_rpn
|
|
5
|
+
from blockchainkit.vm.systems.programs import batch_transfer, vending_machine
|
|
6
|
+
from blockchainkit.vm.systems.reentrancy import drain_bank
|
|
7
|
+
from blockchainkit.vm.systems.script import (
|
|
8
|
+
encode_public_key,
|
|
9
|
+
encode_signature,
|
|
10
|
+
htlc_locking,
|
|
11
|
+
number,
|
|
12
|
+
p2pkh_locking,
|
|
13
|
+
p2pkh_unlocking,
|
|
14
|
+
verify_script,
|
|
15
|
+
)
|
|
16
|
+
from blockchainkit.vm.systems.stack_machine import execute, validate_program
|
|
17
|
+
from blockchainkit.vm.systems.turing import busy_beaver, enumerate_machines, run_turing_machine
|
|
18
|
+
from blockchainkit.vm.systems.verifier import verify_bytecode
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"assemble",
|
|
22
|
+
"compile_expression",
|
|
23
|
+
"to_rpn",
|
|
24
|
+
"batch_transfer",
|
|
25
|
+
"vending_machine",
|
|
26
|
+
"drain_bank",
|
|
27
|
+
"encode_public_key",
|
|
28
|
+
"encode_signature",
|
|
29
|
+
"htlc_locking",
|
|
30
|
+
"number",
|
|
31
|
+
"p2pkh_locking",
|
|
32
|
+
"p2pkh_unlocking",
|
|
33
|
+
"verify_script",
|
|
34
|
+
"execute",
|
|
35
|
+
"validate_program",
|
|
36
|
+
"busy_beaver",
|
|
37
|
+
"enumerate_machines",
|
|
38
|
+
"run_turing_machine",
|
|
39
|
+
"verify_bytecode",
|
|
40
|
+
]
|