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,273 @@
|
|
|
1
|
+
"""Domain-separated Merkle trees with explicit leaf-count commitments.
|
|
2
|
+
|
|
3
|
+
Odd nodes are promoted unchanged. Roots bind the leaf count to avoid
|
|
4
|
+
ambiguity between differently shaped trees. This is not Bitcoin's format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
from blockchainkit._validation import integer
|
|
10
|
+
from blockchainkit.constants import (
|
|
11
|
+
MERKLE_LEAF_PREFIX,
|
|
12
|
+
MERKLE_NODE_PREFIX,
|
|
13
|
+
MERKLE_ROOT_PREFIX,
|
|
14
|
+
UINT64_LIMIT,
|
|
15
|
+
)
|
|
16
|
+
from blockchainkit.crypto.systems.hashing import hash256, sha256
|
|
17
|
+
from blockchainkit.structures.core.base import MerkleProof, MerkleTrace, ProofStep
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _root(count: int, digest: bytes) -> bytes:
|
|
21
|
+
return sha256(MERKLE_ROOT_PREFIX + count.to_bytes(8, "big") + digest)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MerkleTree:
|
|
25
|
+
"""Build a tree from an iterable of byte strings.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
leaves : iterable of bytes
|
|
30
|
+
Ordered payloads, hashed as SHA256(0x00 || payload).
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
>>> from blockchainkit.structures import MerkleTree, verify_proof
|
|
35
|
+
>>> tree = MerkleTree([b"alice", b"bob", b"carol"])
|
|
36
|
+
>>> verify_proof(b"bob", tree.proof(1), tree.root)
|
|
37
|
+
True
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, leaves: Iterable[bytes]) -> None:
|
|
41
|
+
payloads = tuple(leaves)
|
|
42
|
+
if any(not isinstance(leaf, bytes) for leaf in payloads):
|
|
43
|
+
raise TypeError("leaves must be bytes")
|
|
44
|
+
self._count = len(payloads)
|
|
45
|
+
level = tuple(sha256(MERKLE_LEAF_PREFIX + leaf) for leaf in payloads)
|
|
46
|
+
levels = [level]
|
|
47
|
+
while len(level) > 1:
|
|
48
|
+
level = tuple(
|
|
49
|
+
sha256(MERKLE_NODE_PREFIX + level[i] + level[i + 1])
|
|
50
|
+
if i + 1 < len(level)
|
|
51
|
+
else level[i]
|
|
52
|
+
for i in range(0, len(level), 2)
|
|
53
|
+
)
|
|
54
|
+
levels.append(level)
|
|
55
|
+
self._levels = tuple(levels)
|
|
56
|
+
self._root = _root(self._count, level[0] if level else b"")
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return f"MerkleTree(leaves={self._count}, root={self._root.hex()[:16]}...)"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def root(self) -> bytes:
|
|
63
|
+
"""Return the immutable 32-byte, leaf-count-bound root."""
|
|
64
|
+
return self._root
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def leaf_count(self) -> int:
|
|
68
|
+
"""Return the number of leaves the root commits to."""
|
|
69
|
+
return self._count
|
|
70
|
+
|
|
71
|
+
def consistency_proof(self, old_size: int) -> tuple[bytes, ...]:
|
|
72
|
+
"""Prove that this tree extends its first ``old_size`` leaves (RFC 6962)."""
|
|
73
|
+
return consistency_proof(self, old_size)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def levels(self) -> tuple[tuple[bytes, ...], ...]:
|
|
77
|
+
"""Return every level's digests, from leaf hashes up to the top digest.
|
|
78
|
+
|
|
79
|
+
The top digest is not yet the root: the root also binds the leaf count.
|
|
80
|
+
An empty tree has a single empty level.
|
|
81
|
+
"""
|
|
82
|
+
return self._levels
|
|
83
|
+
|
|
84
|
+
def proof(self, index: int) -> MerkleProof:
|
|
85
|
+
"""Return a logarithmic-size inclusion proof at a zero-based position."""
|
|
86
|
+
if type(index) is not int or not 0 <= index < self._count:
|
|
87
|
+
raise IndexError("leaf index out of range")
|
|
88
|
+
siblings = []
|
|
89
|
+
position = index
|
|
90
|
+
for level in self._levels[:-1]:
|
|
91
|
+
sibling = position ^ 1
|
|
92
|
+
siblings.append(level[sibling] if sibling < len(level) else None)
|
|
93
|
+
position //= 2
|
|
94
|
+
return MerkleProof(index, self._count, tuple(siblings))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _walk(leaf: bytes, proof: MerkleProof) -> tuple[bytes, list[ProofStep]] | None:
|
|
98
|
+
"""Recompute the top digest bottom-up; return None for a malformed proof."""
|
|
99
|
+
if not isinstance(leaf, bytes) or not isinstance(proof, MerkleProof):
|
|
100
|
+
return None
|
|
101
|
+
if type(proof.leaf_count) is not int or not 0 < proof.leaf_count < UINT64_LIMIT:
|
|
102
|
+
return None
|
|
103
|
+
if type(proof.index) is not int or not 0 <= proof.index < proof.leaf_count:
|
|
104
|
+
return None
|
|
105
|
+
if not isinstance(proof.siblings, tuple):
|
|
106
|
+
return None
|
|
107
|
+
if len(proof.siblings) != (proof.leaf_count - 1).bit_length():
|
|
108
|
+
return None
|
|
109
|
+
leaf_digest = sha256(MERKLE_LEAF_PREFIX + leaf)
|
|
110
|
+
current, index, count = leaf_digest, proof.index, proof.leaf_count
|
|
111
|
+
steps = []
|
|
112
|
+
# The checked path length ensures count > 1 at every iteration, and
|
|
113
|
+
# repeated ceiling-halving reaches exactly 1 after the final sibling.
|
|
114
|
+
for sibling in proof.siblings:
|
|
115
|
+
if (index ^ 1) >= count:
|
|
116
|
+
if sibling is not None:
|
|
117
|
+
return None
|
|
118
|
+
side = "promoted"
|
|
119
|
+
else:
|
|
120
|
+
if not isinstance(sibling, bytes) or len(sibling) != 32:
|
|
121
|
+
return None
|
|
122
|
+
side = "left" if index % 2 else "right"
|
|
123
|
+
left, right = (sibling, current) if side == "left" else (current, sibling)
|
|
124
|
+
current = sha256(MERKLE_NODE_PREFIX + left + right)
|
|
125
|
+
steps.append(ProofStep(side, sibling, current))
|
|
126
|
+
index //= 2
|
|
127
|
+
count = (count + 1) // 2
|
|
128
|
+
return leaf_digest, steps
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def verify_proof(leaf: bytes, proof: MerkleProof, root: bytes) -> bool:
|
|
132
|
+
"""Verify payload, position, shape, count, and root; reject malformed proofs."""
|
|
133
|
+
if not isinstance(root, bytes) or len(root) != 32:
|
|
134
|
+
return False
|
|
135
|
+
walked = _walk(leaf, proof)
|
|
136
|
+
if walked is None:
|
|
137
|
+
return False
|
|
138
|
+
leaf_digest, steps = walked
|
|
139
|
+
top = steps[-1].digest if steps else leaf_digest
|
|
140
|
+
return _root(proof.leaf_count, top) == root
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def trace_proof(leaf: bytes, proof: MerkleProof, root: bytes) -> MerkleTrace:
|
|
144
|
+
"""Reconstruct the root step by step, recording each level.
|
|
145
|
+
|
|
146
|
+
Unlike :func:`verify_proof`, a well-formed proof that reconstructs the
|
|
147
|
+
wrong root still returns its full trace (with ``valid=False``), so an
|
|
148
|
+
experiment can show where a tampered leaf diverges.
|
|
149
|
+
|
|
150
|
+
Raises
|
|
151
|
+
------
|
|
152
|
+
ValueError
|
|
153
|
+
The proof is malformed: wrong shape, count, index, or sibling type.
|
|
154
|
+
|
|
155
|
+
Examples
|
|
156
|
+
--------
|
|
157
|
+
>>> from blockchainkit.structures import MerkleTree, trace_proof
|
|
158
|
+
>>> tree = MerkleTree([b"a", b"b", b"c"])
|
|
159
|
+
>>> [step.side for step in trace_proof(b"c", tree.proof(2), tree.root).steps]
|
|
160
|
+
['promoted', 'left']
|
|
161
|
+
"""
|
|
162
|
+
walked = _walk(leaf, proof)
|
|
163
|
+
if walked is None:
|
|
164
|
+
raise ValueError("malformed Merkle proof")
|
|
165
|
+
leaf_digest, steps = walked
|
|
166
|
+
top = steps[-1].digest if steps else leaf_digest
|
|
167
|
+
computed = _root(proof.leaf_count, top)
|
|
168
|
+
return MerkleTrace(leaf_digest, tuple(steps), computed, computed == root)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _subtree(digests: list[bytes]) -> bytes:
|
|
172
|
+
"""Top digest of consecutive leaf digests (the RFC 6962 tree shape)."""
|
|
173
|
+
if len(digests) == 1:
|
|
174
|
+
return digests[0]
|
|
175
|
+
split = 1 << ((len(digests) - 1).bit_length() - 1) # Largest power of two below n.
|
|
176
|
+
return sha256(MERKLE_NODE_PREFIX + _subtree(digests[:split]) + _subtree(digests[split:]))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _subproof(m: int, digests: list[bytes], complete: bool) -> list[bytes]:
|
|
180
|
+
n = len(digests)
|
|
181
|
+
if m == n:
|
|
182
|
+
return [] if complete else [_subtree(digests)]
|
|
183
|
+
split = 1 << ((n - 1).bit_length() - 1)
|
|
184
|
+
if m <= split:
|
|
185
|
+
return _subproof(m, digests[:split], complete) + [_subtree(digests[split:])]
|
|
186
|
+
return _subproof(m - split, digests[split:], False) + [_subtree(digests[:split])]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def consistency_proof(tree: MerkleTree, old_size: int) -> tuple[bytes, ...]:
|
|
190
|
+
"""Prove that the first ``old_size`` leaves of ``tree`` form an earlier tree.
|
|
191
|
+
|
|
192
|
+
The proof (RFC 6962, section 2.1.2) lists the subtree digests needed to
|
|
193
|
+
rebuild both the old and the new top digest from shared parts, so a
|
|
194
|
+
verifier can check that the log only appended. Because blockchainkit's
|
|
195
|
+
roots also bind the leaf count, the verifier cannot read the old top
|
|
196
|
+
digest off the old root; when ``old_size`` is a power of two (the case
|
|
197
|
+
where RFC 6962 omits it) the proof starts with it. Also available as
|
|
198
|
+
:meth:`MerkleTree.consistency_proof`.
|
|
199
|
+
"""
|
|
200
|
+
integer(old_size, "old_size", 1)
|
|
201
|
+
if old_size > tree.leaf_count:
|
|
202
|
+
raise ValueError("old_size cannot exceed the tree's leaf count")
|
|
203
|
+
digests = list(tree.levels[0])
|
|
204
|
+
if old_size == tree.leaf_count:
|
|
205
|
+
return ()
|
|
206
|
+
proof = _subproof(old_size, digests, True)
|
|
207
|
+
if old_size & (old_size - 1) == 0:
|
|
208
|
+
proof.insert(0, _subtree(digests[:old_size]))
|
|
209
|
+
return tuple(proof)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def verify_consistency(
|
|
213
|
+
old_size: int, old_root: bytes, new_size: int, new_root: bytes, proof: tuple[bytes, ...]
|
|
214
|
+
) -> bool:
|
|
215
|
+
"""Check that the tree with ``old_root`` is a prefix of the tree with ``new_root``.
|
|
216
|
+
|
|
217
|
+
Follows RFC 9162, section 2.1.4.2, on top digests, then checks both
|
|
218
|
+
count-bound roots. Malformed or mismatched proofs return False.
|
|
219
|
+
|
|
220
|
+
Examples
|
|
221
|
+
--------
|
|
222
|
+
>>> from blockchainkit.structures import MerkleTree, verify_consistency
|
|
223
|
+
>>> leaves = [bytes([i]) for i in range(7)]
|
|
224
|
+
>>> old, new = MerkleTree(leaves[:3]), MerkleTree(leaves)
|
|
225
|
+
>>> verify_consistency(3, old.root, 7, new.root, new.consistency_proof(3))
|
|
226
|
+
True
|
|
227
|
+
"""
|
|
228
|
+
if type(old_size) is not int or type(new_size) is not int or not 0 < old_size <= new_size:
|
|
229
|
+
return False
|
|
230
|
+
if any(not isinstance(digest, bytes) or len(digest) != 32 for digest in proof):
|
|
231
|
+
return False
|
|
232
|
+
if old_size == new_size:
|
|
233
|
+
return not proof and old_root == new_root
|
|
234
|
+
if not proof:
|
|
235
|
+
return False
|
|
236
|
+
fn, sn = old_size - 1, new_size - 1
|
|
237
|
+
while fn & 1:
|
|
238
|
+
fn, sn = fn >> 1, sn >> 1
|
|
239
|
+
old_top = new_top = proof[0]
|
|
240
|
+
for digest in proof[1:]:
|
|
241
|
+
if sn == 0:
|
|
242
|
+
return False
|
|
243
|
+
if fn & 1 or fn == sn:
|
|
244
|
+
old_top = sha256(MERKLE_NODE_PREFIX + digest + old_top)
|
|
245
|
+
new_top = sha256(MERKLE_NODE_PREFIX + digest + new_top)
|
|
246
|
+
while not fn & 1 and fn:
|
|
247
|
+
fn, sn = fn >> 1, sn >> 1
|
|
248
|
+
else:
|
|
249
|
+
new_top = sha256(MERKLE_NODE_PREFIX + new_top + digest)
|
|
250
|
+
fn, sn = fn >> 1, sn >> 1
|
|
251
|
+
return sn == 0 and _root(old_size, old_top) == old_root and _root(new_size, new_top) == new_root
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def bitcoin_merkle_root(leaves: Iterable[bytes]) -> bytes:
|
|
255
|
+
"""Return a root in Bitcoin's convention: double SHA-256, odd nodes duplicated.
|
|
256
|
+
|
|
257
|
+
Shown for contrast with :class:`MerkleTree`. With no domain separation
|
|
258
|
+
and no leaf count, the lists ``[a, b, c]`` and ``[a, b, c, c]`` share a
|
|
259
|
+
root (CVE-2012-2459), which let an attacker make nodes reject a valid
|
|
260
|
+
block. :class:`MerkleTree` promotes odd nodes and binds the count instead.
|
|
261
|
+
|
|
262
|
+
>>> from blockchainkit.structures import bitcoin_merkle_root
|
|
263
|
+
>>> bitcoin_merkle_root([b"a", b"b", b"c"]) == bitcoin_merkle_root([b"a", b"b", b"c", b"c"])
|
|
264
|
+
True
|
|
265
|
+
"""
|
|
266
|
+
level = [hash256(leaf) for leaf in leaves]
|
|
267
|
+
if not level:
|
|
268
|
+
raise ValueError("Bitcoin's convention has no root for an empty list")
|
|
269
|
+
while len(level) > 1:
|
|
270
|
+
if len(level) % 2:
|
|
271
|
+
level.append(level[-1])
|
|
272
|
+
level = [hash256(level[i] + level[i + 1]) for i in range(0, len(level), 2)]
|
|
273
|
+
return level[0]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Merkle mountain ranges (2016): an append-only log with cheap updates.
|
|
2
|
+
|
|
3
|
+
A Merkle tree over n leaves is rebuilt along a whole path when a leaf is
|
|
4
|
+
added. A mountain range keeps a list of perfect binary trees ("peaks") of
|
|
5
|
+
decreasing size, one per 1-bit of n. Appending a leaf adds a peak of size
|
|
6
|
+
one and merges equal-sized neighbours, like binary addition: on average a
|
|
7
|
+
constant number of merges. Old peaks are never rewritten, so proofs about
|
|
8
|
+
old leaves need only the peaks to be refreshed. The root "bags" the peaks.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from blockchainkit.constants import MERKLE_LEAF_PREFIX, MERKLE_NODE_PREFIX, MMR_ROOT_PREFIX
|
|
12
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
13
|
+
from blockchainkit.structures.core.base import MMRProof
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _node(left: bytes, right: bytes) -> bytes:
|
|
17
|
+
return sha256(MERKLE_NODE_PREFIX + left + right)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _bag(size: int, peaks: tuple[bytes, ...]) -> bytes:
|
|
21
|
+
return sha256(MMR_ROOT_PREFIX + size.to_bytes(8, "big") + b"".join(peaks))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _peak_ranges(size: int) -> list[tuple[int, int]]:
|
|
25
|
+
"""(start, leaf count) of each peak, largest first: the 1-bits of ``size``."""
|
|
26
|
+
ranges, start = [], 0
|
|
27
|
+
for bit in reversed(range(size.bit_length())):
|
|
28
|
+
if size >> bit & 1:
|
|
29
|
+
ranges.append((start, 1 << bit))
|
|
30
|
+
start += 1 << bit
|
|
31
|
+
return ranges
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MerkleMountainRange:
|
|
35
|
+
"""An immutable append-only accumulator of byte strings.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> from blockchainkit.structures import MerkleMountainRange, verify_mmr_proof
|
|
40
|
+
>>> mmr = MerkleMountainRange()
|
|
41
|
+
>>> for leaf in (b"a", b"b", b"c"):
|
|
42
|
+
... mmr = mmr.append(leaf)
|
|
43
|
+
>>> len(mmr.peaks), verify_mmr_proof(b"c", mmr.proof(2), mmr.root)
|
|
44
|
+
(2, True)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._leaves: tuple[bytes, ...] = ()
|
|
49
|
+
self._peaks: tuple[tuple[int, bytes], ...] = () # (leaf count, digest)
|
|
50
|
+
|
|
51
|
+
def __len__(self) -> int:
|
|
52
|
+
return len(self._leaves)
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return f"MerkleMountainRange(leaves={len(self)}, peaks={len(self._peaks)})"
|
|
56
|
+
|
|
57
|
+
def append(self, leaf: bytes) -> "MerkleMountainRange":
|
|
58
|
+
"""Return a range with one more leaf, merging equal-sized peaks.
|
|
59
|
+
|
|
60
|
+
Only the new leaf's hash and the merged nodes are computed: about
|
|
61
|
+
two hashes per append on average, however long the range.
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(leaf, bytes):
|
|
64
|
+
raise TypeError("leaves must be bytes")
|
|
65
|
+
peaks = [*self._peaks, (1, sha256(MERKLE_LEAF_PREFIX + leaf))]
|
|
66
|
+
while len(peaks) > 1 and peaks[-1][0] == peaks[-2][0]:
|
|
67
|
+
(count, left), (_, right) = peaks[-2], peaks[-1]
|
|
68
|
+
peaks[-2:] = [(2 * count, _node(left, right))]
|
|
69
|
+
grown = MerkleMountainRange()
|
|
70
|
+
grown._leaves, grown._peaks = (*self._leaves, leaf), tuple(peaks)
|
|
71
|
+
return grown
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def peaks(self) -> tuple[bytes, ...]:
|
|
75
|
+
"""Peak digests, largest tree first; there is one per 1-bit of the size."""
|
|
76
|
+
return tuple(digest for _, digest in self._peaks)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def root(self) -> bytes:
|
|
80
|
+
"""Bag the peaks, with the size, into one commitment."""
|
|
81
|
+
return _bag(len(self), self.peaks)
|
|
82
|
+
|
|
83
|
+
def proof(self, index: int) -> MMRProof:
|
|
84
|
+
"""Return an inclusion proof for the leaf at ``index``."""
|
|
85
|
+
if type(index) is not int or not 0 <= index < len(self):
|
|
86
|
+
raise IndexError("leaf index out of range")
|
|
87
|
+
start, count = next((s, n) for s, n in _peak_ranges(len(self)) if s <= index < s + n)
|
|
88
|
+
level = [sha256(MERKLE_LEAF_PREFIX + leaf) for leaf in self._leaves[start : start + count]]
|
|
89
|
+
position, siblings = index - start, []
|
|
90
|
+
while len(level) > 1:
|
|
91
|
+
siblings.append(level[position ^ 1])
|
|
92
|
+
level = [_node(level[i], level[i + 1]) for i in range(0, len(level), 2)]
|
|
93
|
+
position //= 2
|
|
94
|
+
return MMRProof(index, len(self), tuple(siblings), self.peaks)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def verify_mmr_proof(leaf: bytes, proof: MMRProof, root: bytes) -> bool:
|
|
98
|
+
"""Rebuild the leaf's peak, find it among the peaks, and check the bagged root."""
|
|
99
|
+
ranges = _peak_ranges(proof.size)
|
|
100
|
+
if len(proof.peaks) != len(ranges) or _bag(proof.size, proof.peaks) != root:
|
|
101
|
+
return False
|
|
102
|
+
found = [(i, s, n) for i, (s, n) in enumerate(ranges) if s <= proof.index < s + n]
|
|
103
|
+
if not found or not isinstance(leaf, bytes):
|
|
104
|
+
return False
|
|
105
|
+
peak_index, start, count = found[0]
|
|
106
|
+
if len(proof.siblings) != count.bit_length() - 1:
|
|
107
|
+
return False
|
|
108
|
+
current, position = sha256(MERKLE_LEAF_PREFIX + leaf), proof.index - start
|
|
109
|
+
for sibling in proof.siblings:
|
|
110
|
+
current = _node(sibling, current) if position & 1 else _node(current, sibling)
|
|
111
|
+
position //= 2
|
|
112
|
+
return current == proof.peaks[peak_index]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Sparse Merkle trees (2016): a key-value map with membership and non-membership proofs.
|
|
2
|
+
|
|
3
|
+
Picture a Merkle tree with one leaf for every possible 256-bit key, almost
|
|
4
|
+
all empty. Each key's position is fixed by its hash, so the root is
|
|
5
|
+
independent of insertion order, and absence is provable: show that the leaf
|
|
6
|
+
at the key's position is empty. Empty subtrees have precomputed default
|
|
7
|
+
digests, so only paths to non-empty leaves are ever hashed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from functools import cache, cached_property
|
|
11
|
+
|
|
12
|
+
from blockchainkit._validation import integer
|
|
13
|
+
from blockchainkit.constants import MERKLE_LEAF_PREFIX, MERKLE_NODE_PREFIX
|
|
14
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
15
|
+
from blockchainkit.structures.core.base import SparseMerkleProof
|
|
16
|
+
|
|
17
|
+
_EMPTY_LEAF = bytes(32)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cache
|
|
21
|
+
def _default(height: int) -> bytes:
|
|
22
|
+
"""Digest of an empty subtree of the given height."""
|
|
23
|
+
if height == 0:
|
|
24
|
+
return _EMPTY_LEAF
|
|
25
|
+
below = _default(height - 1)
|
|
26
|
+
return sha256(MERKLE_NODE_PREFIX + below + below)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _path(key: bytes, depth: int) -> int:
|
|
30
|
+
return int.from_bytes(sha256(key), "big") >> (256 - depth)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _leaf(key: bytes, value: bytes) -> bytes:
|
|
34
|
+
return sha256(MERKLE_LEAF_PREFIX + len(key).to_bytes(8, "big") + key + value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SparseMerkleTree:
|
|
38
|
+
"""An immutable map from byte keys to byte values with a Merkle root.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
depth : int
|
|
43
|
+
Number of key-hash bits used for the position, 1 to 256. A small
|
|
44
|
+
depth makes collisions between keys possible; it exists only to make
|
|
45
|
+
small experiments easy to draw.
|
|
46
|
+
|
|
47
|
+
Examples
|
|
48
|
+
--------
|
|
49
|
+
>>> from blockchainkit.structures import SparseMerkleTree, verify_sparse_proof
|
|
50
|
+
>>> tree = SparseMerkleTree().set(b"alice", b"100")
|
|
51
|
+
>>> verify_sparse_proof(tree.prove(b"bob"), tree.root) # Proof that bob is absent.
|
|
52
|
+
True
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, depth: int = 256, items: dict[bytes, bytes] | None = None) -> None:
|
|
56
|
+
integer(depth, "depth", 1)
|
|
57
|
+
if depth > 256:
|
|
58
|
+
raise ValueError("depth cannot exceed 256")
|
|
59
|
+
self._depth = depth
|
|
60
|
+
self._items = dict(items or {})
|
|
61
|
+
self._leaves = {_path(k, depth): _leaf(k, v) for k, v in self._items.items()}
|
|
62
|
+
|
|
63
|
+
def __len__(self) -> int:
|
|
64
|
+
return len(self._items)
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
root = self.root.hex()[:16]
|
|
68
|
+
return f"SparseMerkleTree(depth={self._depth}, keys={len(self)}, root={root}...)"
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _check(key: bytes, value: bytes = b"") -> None:
|
|
72
|
+
if not isinstance(key, bytes) or not isinstance(value, bytes):
|
|
73
|
+
raise TypeError("keys and values must be bytes")
|
|
74
|
+
|
|
75
|
+
def get(self, key: bytes) -> bytes | None:
|
|
76
|
+
"""Return the value stored at a key, or None."""
|
|
77
|
+
self._check(key)
|
|
78
|
+
return self._items.get(key)
|
|
79
|
+
|
|
80
|
+
def set(self, key: bytes, value: bytes) -> "SparseMerkleTree":
|
|
81
|
+
"""Return a new tree with ``key`` mapped to ``value``."""
|
|
82
|
+
self._check(key, value)
|
|
83
|
+
return SparseMerkleTree(self._depth, {**self._items, key: value})
|
|
84
|
+
|
|
85
|
+
def delete(self, key: bytes) -> "SparseMerkleTree":
|
|
86
|
+
"""Return a new tree without ``key``."""
|
|
87
|
+
self._check(key)
|
|
88
|
+
return SparseMerkleTree(self._depth, {k: v for k, v in self._items.items() if k != key})
|
|
89
|
+
|
|
90
|
+
def _digest(self, height: int, prefix: int, leaves: dict[int, bytes]) -> bytes:
|
|
91
|
+
"""Digest of the subtree of ``height`` whose positions start with ``prefix``."""
|
|
92
|
+
if not leaves:
|
|
93
|
+
return _default(height)
|
|
94
|
+
if height == 0:
|
|
95
|
+
return leaves[prefix]
|
|
96
|
+
left_prefix, right_prefix = prefix << 1, (prefix << 1) | 1
|
|
97
|
+
shift = height - 1
|
|
98
|
+
left = {p: d for p, d in leaves.items() if p >> shift == left_prefix}
|
|
99
|
+
right = {p: d for p, d in leaves.items() if p >> shift == right_prefix}
|
|
100
|
+
return sha256(
|
|
101
|
+
MERKLE_NODE_PREFIX
|
|
102
|
+
+ self._digest(shift, left_prefix, left)
|
|
103
|
+
+ self._digest(shift, right_prefix, right)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@cached_property
|
|
107
|
+
def root(self) -> bytes:
|
|
108
|
+
"""Return the root digest; equal maps give equal roots, in any order."""
|
|
109
|
+
return self._digest(self._depth, 0, self._leaves)
|
|
110
|
+
|
|
111
|
+
def prove(self, key: bytes) -> SparseMerkleProof:
|
|
112
|
+
"""Return a proof of the key's value, or of its absence."""
|
|
113
|
+
self._check(key)
|
|
114
|
+
position = _path(key, self._depth)
|
|
115
|
+
siblings = []
|
|
116
|
+
for height in range(self._depth):
|
|
117
|
+
sibling_prefix = (position >> height) ^ 1
|
|
118
|
+
subtree = {p: d for p, d in self._leaves.items() if p >> height == sibling_prefix}
|
|
119
|
+
siblings.append(self._digest(height, sibling_prefix, subtree))
|
|
120
|
+
return SparseMerkleProof(key, self._items.get(key), tuple(siblings))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def verify_sparse_proof(proof: SparseMerkleProof, root: bytes) -> bool:
|
|
124
|
+
"""Recompute the root from a key, its claimed value (or absence), and siblings.
|
|
125
|
+
|
|
126
|
+
The number of siblings is the tree depth. Leaf and node digests carry
|
|
127
|
+
different prefixes, so a shorter proof cannot pass for a longer one.
|
|
128
|
+
"""
|
|
129
|
+
depth = len(proof.siblings)
|
|
130
|
+
if not 1 <= depth <= 256 or any(not isinstance(d, bytes) for d in proof.siblings):
|
|
131
|
+
return False
|
|
132
|
+
position = _path(proof.key, depth)
|
|
133
|
+
current = _EMPTY_LEAF if proof.value is None else _leaf(proof.key, proof.value)
|
|
134
|
+
for height, sibling in enumerate(proof.siblings):
|
|
135
|
+
if (position >> height) & 1:
|
|
136
|
+
current = sha256(MERKLE_NODE_PREFIX + sibling + current)
|
|
137
|
+
else:
|
|
138
|
+
current = sha256(MERKLE_NODE_PREFIX + current + sibling)
|
|
139
|
+
return current == root
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Immutable signed account transfers and deterministic teaching encodings."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
|
|
5
|
+
from blockchainkit._validation import integer
|
|
6
|
+
from blockchainkit.constants import DEFAULT_CHAIN_ID, UINT64_LIMIT
|
|
7
|
+
from blockchainkit.crypto import (
|
|
8
|
+
SchnorrSignature,
|
|
9
|
+
encode_point,
|
|
10
|
+
public_key,
|
|
11
|
+
sha256,
|
|
12
|
+
sign,
|
|
13
|
+
verify,
|
|
14
|
+
)
|
|
15
|
+
from blockchainkit.structures.utils.accounts import is_account_id
|
|
16
|
+
from blockchainkit.structures.utils.encoding import canonical_json
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def address(public: tuple[int, int]) -> str:
|
|
20
|
+
"""Return SHA-256(uncompressed public key) as a 64-character account ID."""
|
|
21
|
+
return sha256(encode_point(public)).hex()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Transaction:
|
|
26
|
+
"""An integer-valued transfer, signed over network ID and account nonce.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
sender : tuple of int
|
|
31
|
+
secp256k1 public point.
|
|
32
|
+
recipient : str
|
|
33
|
+
Lowercase 64-character hexadecimal account identifier.
|
|
34
|
+
amount : int
|
|
35
|
+
Positive integer units (no floating-point currency amounts).
|
|
36
|
+
nonce : int
|
|
37
|
+
Sender sequence number, starting at zero.
|
|
38
|
+
chain_id : str
|
|
39
|
+
Domain that prevents replay onto a different teaching network.
|
|
40
|
+
signature : SchnorrSignature, optional
|
|
41
|
+
Signature over the canonical unsigned payload.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
sender: tuple[int, int]
|
|
45
|
+
recipient: str
|
|
46
|
+
amount: int
|
|
47
|
+
nonce: int
|
|
48
|
+
chain_id: str = DEFAULT_CHAIN_ID
|
|
49
|
+
signature: SchnorrSignature | None = None
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
encode_point(self.sender)
|
|
53
|
+
if not is_account_id(self.recipient):
|
|
54
|
+
raise ValueError("recipient must be a lowercase SHA-256 account identifier")
|
|
55
|
+
integer(self.amount, "amount", 1)
|
|
56
|
+
integer(self.nonce, "nonce")
|
|
57
|
+
if self.amount >= UINT64_LIMIT or self.nonce >= UINT64_LIMIT:
|
|
58
|
+
raise ValueError("amount and nonce must fit unsigned 64-bit integers")
|
|
59
|
+
if not isinstance(self.chain_id, str) or not 1 <= len(self.chain_id) <= 128:
|
|
60
|
+
raise ValueError("chain_id must contain 1 to 128 characters")
|
|
61
|
+
if self.signature is not None and not isinstance(self.signature, SchnorrSignature):
|
|
62
|
+
raise TypeError("signature must be a SchnorrSignature or None")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def sender_address(self) -> str:
|
|
66
|
+
"""Return the account identifier derived from the sender's public key."""
|
|
67
|
+
return address(self.sender)
|
|
68
|
+
|
|
69
|
+
def payload(self) -> bytes:
|
|
70
|
+
"""Return the exact bytes signed, including domain and format version."""
|
|
71
|
+
return canonical_json(
|
|
72
|
+
{
|
|
73
|
+
"version": 1,
|
|
74
|
+
"chain_id": self.chain_id,
|
|
75
|
+
"sender": encode_point(self.sender).hex(),
|
|
76
|
+
"recipient": self.recipient,
|
|
77
|
+
"amount": self.amount,
|
|
78
|
+
"nonce": self.nonce,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def to_bytes(self) -> bytes:
|
|
83
|
+
"""Serialize the unsigned payload and signature without ambiguity."""
|
|
84
|
+
signature = None
|
|
85
|
+
if self.signature is not None:
|
|
86
|
+
signature = [list(self.signature.commitment), self.signature.response]
|
|
87
|
+
return canonical_json({"payload": self.payload().decode(), "signature": signature})
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def txid(self) -> bytes:
|
|
91
|
+
"""Return a digest of the signed transaction encoding.
|
|
92
|
+
|
|
93
|
+
Because it covers the signature, re-signing the same payment changes
|
|
94
|
+
it: the malleability that segregated witness removed from Bitcoin's
|
|
95
|
+
transaction ids. Compare :attr:`unsigned_id`.
|
|
96
|
+
"""
|
|
97
|
+
return sha256(self.to_bytes())
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def unsigned_id(self) -> bytes:
|
|
101
|
+
"""Return a digest of the unsigned payload only, as SegWit's txid does.
|
|
102
|
+
|
|
103
|
+
Any valid signature over the same payment gives the same value, so a
|
|
104
|
+
later transaction can refer to this one before it is confirmed.
|
|
105
|
+
"""
|
|
106
|
+
return sha256(self.payload())
|
|
107
|
+
|
|
108
|
+
def is_valid(self) -> bool:
|
|
109
|
+
"""Check the signature; account balance and nonce are checked by Ledger."""
|
|
110
|
+
return self.signature is not None and verify(self.payload(), self.signature, self.sender)
|
|
111
|
+
|
|
112
|
+
def signed(self, private: int, *, signing_nonce: int | None = None) -> "Transaction":
|
|
113
|
+
"""Return a signed copy, checking that the key matches this sender."""
|
|
114
|
+
if public_key(private) != self.sender:
|
|
115
|
+
raise ValueError("private key does not match sender")
|
|
116
|
+
return replace(self, signature=sign(self.payload(), private, nonce=signing_nonce))
|