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,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))