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,122 @@
1
+ """The CAP trade-off (Brewer 2000; Gilbert and Lynch 2002) on a replicated register.
2
+
3
+ A register is copied on several replicas. When the network partitions them,
4
+ a replica that cannot reach the others must choose: answer from what it
5
+ knows (stay *available*, risking stale reads and lost writes) or refuse
6
+ (stay *consistent*). Gilbert and Lynch proved no system can guarantee both
7
+ during a partition. A blockchain makes the same choice: Bitcoin keeps
8
+ producing blocks on both sides of a split and reconciles later.
9
+ """
10
+
11
+ from collections.abc import Iterable
12
+
13
+ from blockchainkit._validation import integer
14
+ from blockchainkit.network.core.base import Operation
15
+
16
+ MODES = ("consistent", "available")
17
+ """tuple of str: The two policies a replica can follow during a partition."""
18
+
19
+
20
+ class ReplicatedRegister:
21
+ """A string register replicated on ``replicas`` nodes that can be partitioned.
22
+
23
+ Parameters
24
+ ----------
25
+ replicas : int
26
+ Number of replicas, at least 1; they are numbered from 0.
27
+ mode : {"consistent", "available"}
28
+ ``"consistent"``: a request succeeds only if the contacted replica
29
+ reaches a strict majority, and it then writes to or reads from every
30
+ reachable replica. Any two majorities overlap, so a read sees the
31
+ latest successful write. ``"available"``: every request succeeds
32
+ using the replicas the contacted one can reach.
33
+ initial : str
34
+ The value every replica starts with.
35
+
36
+ Notes
37
+ -----
38
+ Every value carries a version ``(counter, replica)``; a write's counter
39
+ is one more than the highest it can see. :meth:`heal` reconnects all
40
+ replicas and keeps the highest version (last writer wins), so in
41
+ available mode a write made on the losing side of a partition is lost.
42
+
43
+ Examples
44
+ --------
45
+ >>> from blockchainkit.network import ReplicatedRegister
46
+ >>> register = ReplicatedRegister(3, mode="consistent")
47
+ >>> register.partition([0], [1, 2])
48
+ >>> register.write(0, "x").ok, register.write(1, "y").ok
49
+ (False, True)
50
+ """
51
+
52
+ def __init__(self, replicas: int, *, mode: str = "consistent", initial: str = "") -> None:
53
+ integer(replicas, "replicas", 1)
54
+ if mode not in MODES:
55
+ raise ValueError(f"mode must be one of {MODES}")
56
+ self._mode = mode
57
+ self._store: list[tuple[tuple[int, int], str]] = [((0, 0), initial)] * replicas
58
+ self._group = [frozenset(range(replicas))] * replicas
59
+ self._history: list[Operation] = []
60
+
61
+ def __repr__(self) -> str:
62
+ return f"ReplicatedRegister(replicas={len(self._store)}, mode={self._mode!r})"
63
+
64
+ @property
65
+ def history(self) -> tuple[Operation, ...]:
66
+ """Every request so far, in order."""
67
+ return tuple(self._history)
68
+
69
+ def values(self) -> tuple[str, ...]:
70
+ """The value each replica currently holds, indexed by replica."""
71
+ return tuple(value for _, value in self._store)
72
+
73
+ def partition(self, *groups: Iterable[int]) -> None:
74
+ """Split the replicas into groups that can talk only within themselves.
75
+
76
+ The groups must cover every replica exactly once.
77
+ """
78
+ sets = [frozenset(group) for group in groups]
79
+ if sorted(r for group in sets for r in group) != list(range(len(self._store))):
80
+ raise ValueError("groups must cover every replica exactly once")
81
+ for group in sets:
82
+ for replica in group:
83
+ self._group[replica] = group
84
+
85
+ def heal(self) -> None:
86
+ """Reconnect every replica and converge on the highest version."""
87
+ self._group = [frozenset(range(len(self._store)))] * len(self._store)
88
+ self._store = [max(self._store)] * len(self._store)
89
+
90
+ def _reachable(self, replica: int) -> frozenset[int] | None:
91
+ integer(replica, "replica")
92
+ if replica >= len(self._store):
93
+ raise ValueError("unknown replica")
94
+ group = self._group[replica]
95
+ if self._mode == "consistent" and 2 * len(group) <= len(self._store):
96
+ return None # No majority: refuse rather than risk inconsistency.
97
+ return group
98
+
99
+ def write(self, replica: int, value: str) -> Operation:
100
+ """Ask ``replica`` to store ``value``; return the recorded operation."""
101
+ if not isinstance(value, str):
102
+ raise TypeError("value must be a str")
103
+ group = self._reachable(replica)
104
+ if group is None:
105
+ operation = Operation("write", replica, None, False)
106
+ else:
107
+ counter = max(self._store[r][0][0] for r in group) + 1
108
+ for r in group:
109
+ self._store[r] = ((counter, replica), value)
110
+ operation = Operation("write", replica, value, True)
111
+ self._history.append(operation)
112
+ return operation
113
+
114
+ def read(self, replica: int) -> Operation:
115
+ """Ask ``replica`` for the value; return the recorded operation."""
116
+ group = self._reachable(replica)
117
+ if group is None:
118
+ operation = Operation("read", replica, None, False)
119
+ else:
120
+ operation = Operation("read", replica, max(self._store[r] for r in group)[1], True)
121
+ self._history.append(operation)
122
+ return operation
@@ -0,0 +1,238 @@
1
+ """Peer graphs: random (Erdős–Rényi 1959), small-world (Watts–Strogatz 1998),
2
+ and scale-free (Barabási–Albert 1999).
3
+
4
+ Who is connected to whom decides how fast news spreads, how many links an
5
+ attacker must cut, and how much a single peer can see. These generators build
6
+ the three classic families so experiments can compare them on equal terms.
7
+ """
8
+
9
+ from collections import deque
10
+ from collections.abc import Iterable
11
+ from random import Random
12
+
13
+ from blockchainkit._validation import integer, probability
14
+ from blockchainkit._validation import seed as check_seed
15
+
16
+
17
+ class Graph:
18
+ """An immutable undirected simple graph on the nodes ``0, 1, ..., n - 1``.
19
+
20
+ Parameters
21
+ ----------
22
+ n : int
23
+ Number of nodes, at least 1.
24
+ edges : iterable of tuple of int
25
+ Unordered pairs of distinct nodes; duplicates are merged.
26
+
27
+ Examples
28
+ --------
29
+ >>> from blockchainkit.network import Graph
30
+ >>> path = Graph(3, [(0, 1), (1, 2)])
31
+ >>> path.distances(0)
32
+ (0, 1, 2)
33
+ >>> path.is_connected()
34
+ True
35
+ """
36
+
37
+ def __init__(self, n: int, edges: Iterable[tuple[int, int]]) -> None:
38
+ integer(n, "n", 1)
39
+ adjacency: list[set[int]] = [set() for _ in range(n)]
40
+ for left, right in edges:
41
+ integer(left, "edge endpoint")
42
+ integer(right, "edge endpoint")
43
+ if left >= n or right >= n or left == right:
44
+ raise ValueError("edges join two distinct nodes in range(n)")
45
+ adjacency[left].add(right)
46
+ adjacency[right].add(left)
47
+ self._neighbors = tuple(tuple(sorted(nodes)) for nodes in adjacency)
48
+ self._adjacent = tuple(frozenset(nodes) for nodes in adjacency)
49
+
50
+ def __repr__(self) -> str:
51
+ return f"Graph(n={self.n}, edges={len(self.edges)})"
52
+
53
+ @property
54
+ def n(self) -> int:
55
+ """Number of nodes."""
56
+ return len(self._neighbors)
57
+
58
+ @property
59
+ def edges(self) -> tuple[tuple[int, int], ...]:
60
+ """Every edge once, as ``(smaller, larger)``, in sorted order."""
61
+ return tuple((u, v) for u in range(self.n) for v in self._neighbors[u] if u < v)
62
+
63
+ def neighbors(self, node: int) -> tuple[int, ...]:
64
+ """Sorted neighbors of ``node``."""
65
+ return self._neighbors[node]
66
+
67
+ def degrees(self) -> tuple[int, ...]:
68
+ """Number of neighbors of every node, indexed by node."""
69
+ return tuple(len(nodes) for nodes in self._neighbors)
70
+
71
+ def distances(self, source: int) -> tuple[int | None, ...]:
72
+ """Hop counts from ``source`` by breadth-first search; None if unreachable."""
73
+ result: list[int | None] = [None] * self.n
74
+ result[source] = 0
75
+ queue = deque([source])
76
+ while queue:
77
+ node = queue.popleft()
78
+ hops = result[node]
79
+ assert hops is not None # Only reached nodes are queued.
80
+ for neighbor in self._neighbors[node]:
81
+ if result[neighbor] is None:
82
+ result[neighbor] = hops + 1
83
+ queue.append(neighbor)
84
+ return tuple(result)
85
+
86
+ def components(self) -> tuple[frozenset[int], ...]:
87
+ """Connected components, largest first (ties by smallest node)."""
88
+ unseen, found = set(range(self.n)), []
89
+ while unseen:
90
+ start = min(unseen)
91
+ reached = frozenset(i for i, d in enumerate(self.distances(start)) if d is not None)
92
+ found.append(reached)
93
+ unseen -= reached
94
+ return tuple(sorted(found, key=lambda c: (-len(c), min(c))))
95
+
96
+ def is_connected(self) -> bool:
97
+ """True if every node can reach every other node."""
98
+ return len(self.components()) == 1
99
+
100
+ def average_path_length(self) -> float:
101
+ """Mean hop count over all ordered pairs of distinct, mutually reachable nodes.
102
+
103
+ This is the *L* of Watts and Strogatz. A graph with no such pair
104
+ (a single node, or no edges) has length 0.
105
+ """
106
+ total = pairs = 0
107
+ for source in range(self.n):
108
+ for d in self.distances(source):
109
+ if d:
110
+ total += d
111
+ pairs += 1
112
+ return total / pairs if pairs else 0.0
113
+
114
+ def clustering(self) -> float:
115
+ """Average local clustering coefficient: the *C* of Watts and Strogatz.
116
+
117
+ A node's coefficient is the fraction of pairs of its neighbors that
118
+ are themselves linked ("my friends know each other"). Nodes with fewer
119
+ than two neighbors contribute 0.
120
+ """
121
+ total = 0.0
122
+ for nodes in self._neighbors:
123
+ k = len(nodes)
124
+ if k >= 2:
125
+ linked = sum(
126
+ 1 for i, u in enumerate(nodes) for v in nodes[i + 1 :] if v in self._adjacent[u]
127
+ )
128
+ total += linked / (k * (k - 1) / 2)
129
+ return total / self.n
130
+
131
+
132
+ def complete_graph(n: int) -> Graph:
133
+ """Every pair of the ``n`` nodes linked: the setting of classic rumor-spreading results.
134
+
135
+ >>> from blockchainkit.network import complete_graph
136
+ >>> len(complete_graph(5).edges)
137
+ 10
138
+ """
139
+ integer(n, "n", 1)
140
+ return Graph(n, ((u, v) for u in range(n) for v in range(u + 1, n)))
141
+
142
+
143
+ def erdos_renyi(n: int, p: float, *, seed: int = 0) -> Graph:
144
+ """Random graph G(n, p): each of the n(n-1)/2 possible links exists with probability p.
145
+
146
+ Erdős and Rényi showed that connectivity appears abruptly: for large n,
147
+ G(n, p) is almost surely disconnected when ``p < (1 - e) ln n / n`` and
148
+ almost surely connected when ``p > (1 + e) ln n / n``.
149
+
150
+ >>> from blockchainkit.network import erdos_renyi
151
+ >>> erdos_renyi(30, 1.0).is_connected()
152
+ True
153
+ """
154
+ integer(n, "n", 1)
155
+ probability(p, "p")
156
+ check_seed(seed)
157
+ random = Random(seed)
158
+ return Graph(n, ((u, v) for u in range(n) for v in range(u + 1, n) if random.random() < p))
159
+
160
+
161
+ def ring_lattice(n: int, k: int) -> Graph:
162
+ """Ring where each node links to its ``k`` nearest nodes, ``k / 2`` on each side.
163
+
164
+ >>> from blockchainkit.network import ring_lattice
165
+ >>> ring_lattice(6, 2).neighbors(0)
166
+ (1, 5)
167
+ """
168
+ integer(n, "n", 3)
169
+ integer(k, "k", 2)
170
+ if k % 2 or k >= n:
171
+ raise ValueError("k must be even and smaller than n")
172
+ return Graph(n, ((u, (u + j) % n) for u in range(n) for j in range(1, k // 2 + 1)))
173
+
174
+
175
+ def watts_strogatz(n: int, k: int, beta: float, *, seed: int = 0) -> Graph:
176
+ """Small-world graph: rewire each ring-lattice edge with probability ``beta``.
177
+
178
+ Following Watts and Strogatz, each edge ``(u, u + j)`` of
179
+ :func:`ring_lattice` keeps its endpoint ``u`` and, with probability
180
+ ``beta``, moves its other end to a uniformly random node, avoiding
181
+ self-loops and duplicate links. A few shortcuts (small ``beta``) shrink
182
+ the average path length almost to that of a random graph while the
183
+ clustering stays close to the lattice's.
184
+
185
+ >>> from blockchainkit.network import watts_strogatz
186
+ >>> len(watts_strogatz(20, 4, 0.3, seed=1).edges)
187
+ 40
188
+ """
189
+ lattice = ring_lattice(n, k)
190
+ probability(beta, "beta")
191
+ check_seed(seed)
192
+ random = Random(seed)
193
+ adjacency = [set(lattice.neighbors(u)) for u in range(n)]
194
+ for j in range(1, k // 2 + 1): # Rewire lap by lap, as in the 1998 paper.
195
+ for u in range(n):
196
+ v = (u + j) % n
197
+ if random.random() >= beta:
198
+ continue
199
+ choices = [w for w in range(n) if w != u and w not in adjacency[u]]
200
+ if not choices:
201
+ continue # u already links to everyone; nothing to rewire to.
202
+ w = random.choice(choices)
203
+ adjacency[u].discard(v)
204
+ adjacency[v].discard(u)
205
+ adjacency[u].add(w)
206
+ adjacency[w].add(u)
207
+ return Graph(n, ((u, v) for u in range(n) for v in adjacency[u] if u < v))
208
+
209
+
210
+ def barabasi_albert(n: int, m: int, *, seed: int = 0) -> Graph:
211
+ """Scale-free graph by preferential attachment.
212
+
213
+ Start from ``m + 1`` fully linked nodes. Each new node links to ``m``
214
+ distinct existing nodes, chosen with probability proportional to their
215
+ current degree ("the rich get richer"). The degree distribution
216
+ approaches the power law ``P(k) ~ k^-3``: a few hubs, many small nodes.
217
+
218
+ >>> from blockchainkit.network import barabasi_albert
219
+ >>> g = barabasi_albert(50, 2, seed=3)
220
+ >>> len(g.edges) == 3 + 2 * (50 - 3)
221
+ True
222
+ """
223
+ integer(m, "m", 1)
224
+ integer(n, "n", m + 1)
225
+ check_seed(seed)
226
+ random = Random(seed)
227
+ edges = [(u, v) for u in range(m + 1) for v in range(u + 1, m + 1)]
228
+ # Each node appears in ``ends`` once per incident edge, so a uniform pick
229
+ # from ``ends`` is a pick proportional to degree.
230
+ ends = [node for edge in edges for node in edge]
231
+ for new in range(m + 1, n):
232
+ targets: set[int] = set()
233
+ while len(targets) < m:
234
+ targets.add(random.choice(ends))
235
+ for target in sorted(targets):
236
+ edges.append((target, new))
237
+ ends.extend((target, new))
238
+ return Graph(n, edges)
@@ -0,0 +1,14 @@
1
+ """Plotting helpers for blockchainkit.network.
2
+
3
+ Imports matplotlib, so ``import blockchainkit`` does not load this module;
4
+ import it explicitly: ``from blockchainkit.network.visualizers import ...``.
5
+ """
6
+
7
+ from blockchainkit.network.visualizers.plots import (
8
+ plot_gossip_timeline,
9
+ plot_graph,
10
+ plot_rumor_spread,
11
+ plot_space_time,
12
+ )
13
+
14
+ __all__ = ["plot_gossip_timeline", "plot_graph", "plot_rumor_spread", "plot_space_time"]
@@ -0,0 +1,177 @@
1
+ """Plotting helpers for blockchainkit.network: graphs, space-time diagrams, and spreading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping, Sequence
6
+
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ from matplotlib.axes import Axes
10
+
11
+ from blockchainkit.network.core.base import Delivery, RumorRun
12
+ from blockchainkit.network.systems.clocks import Event, lamport_timestamps
13
+ from blockchainkit.network.systems.topology import Graph
14
+
15
+ __all__ = ["plot_gossip_timeline", "plot_graph", "plot_rumor_spread", "plot_space_time"]
16
+
17
+
18
+ def plot_gossip_timeline(
19
+ deliveries: Sequence[Delivery], *, payload: bytes | None = None, ax: Axes | None = None
20
+ ) -> Axes:
21
+ """Show when each peer first accepted a payload, earliest at the top.
22
+
23
+ Parameters
24
+ ----------
25
+ deliveries : sequence of Delivery
26
+ Usually ``SimulatedNetwork.deliveries``.
27
+ payload : bytes, optional
28
+ Payload to plot; required when deliveries contain several payloads.
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
+ payloads = {d.payload for d in deliveries}
37
+ if payload is None:
38
+ if len(payloads) != 1:
39
+ raise ValueError("deliveries hold several payloads; choose one with payload=")
40
+ (payload,) = payloads
41
+ chosen = sorted(
42
+ (d for d in deliveries if d.payload == payload), key=lambda d: (d.time, d.recipient)
43
+ )
44
+ if not chosen:
45
+ raise ValueError("no deliveries of that payload")
46
+ if ax is None:
47
+ _, ax = plt.subplots()
48
+ ordered = chosen[::-1] # barh draws bottom-up; keep the earliest peer on top.
49
+ rows = range(len(ordered))
50
+ ax.barh(rows, [d.time for d in ordered], color="#2563eb")
51
+ ax.set_yticks(rows, [d.recipient for d in ordered])
52
+ for row, d in zip(rows, ordered, strict=True):
53
+ source = "origin" if d.sender == d.recipient else f"from {d.sender}"
54
+ ax.text(d.time, row, f" t={d.time} ({source})", va="center", fontsize=8)
55
+ ax.set_xlabel("simulation time of first acceptance")
56
+ ax.set_title(f"Gossip reached {len(chosen)} peers by t={chosen[-1].time}")
57
+ return ax
58
+
59
+
60
+ def plot_graph(graph: Graph, *, highlight: Iterable[int] = (), ax: Axes | None = None) -> Axes:
61
+ """Draw a peer graph with its nodes on a circle.
62
+
63
+ A circle shows the structure of ring lattices and small worlds directly:
64
+ lattice links hug the rim and rewired shortcuts cut across it.
65
+
66
+ Parameters
67
+ ----------
68
+ graph : Graph
69
+ The graph to draw.
70
+ highlight : iterable of int
71
+ Nodes to draw in a contrasting color, such as spies or hubs.
72
+ ax : matplotlib.axes.Axes, optional
73
+ Axes to draw on; a new figure is created if omitted.
74
+
75
+ Returns
76
+ -------
77
+ matplotlib.axes.Axes
78
+ """
79
+ if ax is None:
80
+ _, ax = plt.subplots()
81
+ angles = 2 * np.pi * np.arange(graph.n) / graph.n
82
+ x, y = np.cos(angles), np.sin(angles)
83
+ for u, v in graph.edges:
84
+ ax.plot([x[u], x[v]], [y[u], y[v]], color="#94a3b8", linewidth=0.6, zorder=1)
85
+ marked = set(highlight)
86
+ colors = ["#dc2626" if node in marked else "#2563eb" for node in range(graph.n)]
87
+ ax.scatter(x, y, s=18, c=colors, zorder=2)
88
+ ax.set_aspect("equal")
89
+ ax.set_axis_off()
90
+ ax.set_title(f"{graph.n} peers, {len(graph.edges)} links")
91
+ return ax
92
+
93
+
94
+ def plot_space_time(processes: Sequence[Sequence[Event]], *, ax: Axes | None = None) -> Axes:
95
+ """Draw Lamport's space-time diagram, labeling each event with its Lamport clock.
96
+
97
+ Each process is a horizontal line, time runs left to right, and each
98
+ message is an arrow from its send to its receive. An event is placed at
99
+ its Lamport timestamp, so every arrow points forward in time.
100
+
101
+ Parameters
102
+ ----------
103
+ processes : sequence of sequence of Event
104
+ A history as accepted by
105
+ :func:`~blockchainkit.network.systems.clocks.lamport_timestamps`.
106
+ ax : matplotlib.axes.Axes, optional
107
+ Axes to draw on; a new figure is created if omitted.
108
+
109
+ Returns
110
+ -------
111
+ matplotlib.axes.Axes
112
+ """
113
+ stamps = lamport_timestamps(processes)
114
+ if ax is None:
115
+ _, ax = plt.subplots()
116
+ sends: dict[str, tuple[int, int]] = {}
117
+ for p, events in enumerate(processes):
118
+ ax.axhline(p, color="#cbd5e1", linewidth=1, zorder=0)
119
+ for i, (kind, label) in enumerate(events):
120
+ if kind == "send":
121
+ sends[label] = (stamps[p][i], p)
122
+ for p, events in enumerate(processes):
123
+ for i, (kind, label) in enumerate(events):
124
+ clock = stamps[p][i]
125
+ if kind == "receive":
126
+ start = sends[label]
127
+ ax.annotate(
128
+ "",
129
+ xy=(clock, p),
130
+ xytext=start,
131
+ arrowprops={"arrowstyle": "->", "color": "#2563eb"},
132
+ )
133
+ ax.scatter(clock, p, s=30, color="black", zorder=2)
134
+ ax.annotate(
135
+ f"{label} ({clock})",
136
+ (clock, p),
137
+ xytext=(3, 6),
138
+ textcoords="offset points",
139
+ fontsize=8,
140
+ )
141
+ ax.set_yticks(range(len(processes)), [f"P{p}" for p in range(len(processes))])
142
+ ax.set_xlabel("Lamport time")
143
+ ax.set_title("Space-time diagram")
144
+ return ax
145
+
146
+
147
+ def plot_rumor_spread(runs: Mapping[str, RumorRun], *, ax: Axes | None = None) -> Axes:
148
+ """Plot the fraction of peers still uninformed after each round, on a log scale.
149
+
150
+ On a log scale, push's slow final phase shows as a straight line
151
+ (the residue shrinks by a constant factor per round), while pull's
152
+ accelerating fall shows the residue squaring each round.
153
+
154
+ Parameters
155
+ ----------
156
+ runs : mapping of str to RumorRun
157
+ Runs to compare, keyed by their legend label.
158
+ ax : matplotlib.axes.Axes, optional
159
+ Axes to draw on; a new figure is created if omitted.
160
+
161
+ Returns
162
+ -------
163
+ matplotlib.axes.Axes
164
+ """
165
+ if not runs:
166
+ raise ValueError("provide at least one run")
167
+ if ax is None:
168
+ _, ax = plt.subplots()
169
+ for label, run in runs.items():
170
+ residue = [1 - count / run.n for count in run.informed]
171
+ rounds = [r for r, value in enumerate(residue) if value > 0]
172
+ ax.semilogy(rounds, [residue[r] for r in rounds], marker="o", markersize=3, label=label)
173
+ ax.set_xlabel("round")
174
+ ax.set_ylabel("fraction still uninformed")
175
+ ax.set_title("Rumor spreading")
176
+ ax.legend()
177
+ return ax
blockchainkit/py.typed ADDED
File without changes
@@ -0,0 +1,61 @@
1
+ """Authenticated data, signed transactions, immutable blocks, and fork state."""
2
+
3
+ from blockchainkit.structures.core.base import (
4
+ Coin,
5
+ MerkleProof,
6
+ MerkleTrace,
7
+ MMRProof,
8
+ OutPoint,
9
+ ProofStep,
10
+ SparseMerkleProof,
11
+ )
12
+ from blockchainkit.structures.systems.block import Block, BlockHeader
13
+ from blockchainkit.structures.systems.bloom import BloomFilter
14
+ from blockchainkit.structures.systems.chain import Blockchain
15
+ from blockchainkit.structures.systems.hash_chain import hash_chain, verify_one_time_password
16
+ from blockchainkit.structures.systems.headers import verify_header_chain
17
+ from blockchainkit.structures.systems.ledger import Ledger
18
+ from blockchainkit.structures.systems.merkle import (
19
+ MerkleTree,
20
+ bitcoin_merkle_root,
21
+ consistency_proof,
22
+ trace_proof,
23
+ verify_consistency,
24
+ verify_proof,
25
+ )
26
+ from blockchainkit.structures.systems.mmr import MerkleMountainRange, verify_mmr_proof
27
+ from blockchainkit.structures.systems.sparse_merkle import SparseMerkleTree, verify_sparse_proof
28
+ from blockchainkit.structures.systems.transaction import Transaction, address
29
+ from blockchainkit.structures.systems.utxo import UTXOSet, UTXOTransaction
30
+
31
+ __all__ = [
32
+ "Coin",
33
+ "MMRProof",
34
+ "MerkleProof",
35
+ "MerkleTrace",
36
+ "OutPoint",
37
+ "ProofStep",
38
+ "SparseMerkleProof",
39
+ "Block",
40
+ "BlockHeader",
41
+ "BloomFilter",
42
+ "Blockchain",
43
+ "hash_chain",
44
+ "verify_one_time_password",
45
+ "verify_header_chain",
46
+ "Ledger",
47
+ "MerkleTree",
48
+ "bitcoin_merkle_root",
49
+ "consistency_proof",
50
+ "trace_proof",
51
+ "verify_consistency",
52
+ "verify_proof",
53
+ "MerkleMountainRange",
54
+ "verify_mmr_proof",
55
+ "SparseMerkleTree",
56
+ "verify_sparse_proof",
57
+ "Transaction",
58
+ "address",
59
+ "UTXOSet",
60
+ "UTXOTransaction",
61
+ ]
@@ -0,0 +1,21 @@
1
+ """Result containers for blockchainkit.structures."""
2
+
3
+ from blockchainkit.structures.core.base import (
4
+ Coin,
5
+ MerkleProof,
6
+ MerkleTrace,
7
+ MMRProof,
8
+ OutPoint,
9
+ ProofStep,
10
+ SparseMerkleProof,
11
+ )
12
+
13
+ __all__ = [
14
+ "Coin",
15
+ "MMRProof",
16
+ "MerkleProof",
17
+ "MerkleTrace",
18
+ "OutPoint",
19
+ "ProofStep",
20
+ "SparseMerkleProof",
21
+ ]