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,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
|
+
]
|