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,126 @@
|
|
|
1
|
+
"""Eclipse attacks on peer selection (Heilman, Kendler, Zohar and Goldberg 2015).
|
|
2
|
+
|
|
3
|
+
A node picks its outbound connections at random from a table of addresses
|
|
4
|
+
it has heard of. An attacker who fills that table with its own addresses
|
|
5
|
+
can make every connection land on an attacker: the node is *eclipsed*, and
|
|
6
|
+
sees only the blocks and transactions the attacker chooses to show it.
|
|
7
|
+
|
|
8
|
+
Bitcoin's defense, strengthened after the 2015 paper, is bucketing: an
|
|
9
|
+
address goes to a bucket determined by a secret hash of its network group
|
|
10
|
+
(its /16 IP prefix), and each group can reach only a few buckets. An
|
|
11
|
+
attacker with many addresses but few groups can then fill only a small part
|
|
12
|
+
of the table, however many addresses it sends.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from random import Random
|
|
16
|
+
|
|
17
|
+
from blockchainkit._validation import integer, probability
|
|
18
|
+
from blockchainkit._validation import seed as check_seed
|
|
19
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def eclipse_probability(attacker_fraction: float, outbound: int) -> float:
|
|
23
|
+
"""Chance that all ``outbound`` connections land on attackers: ``f ** outbound``.
|
|
24
|
+
|
|
25
|
+
Each connection is modeled as an independent draw from a table in which
|
|
26
|
+
a fraction ``f`` of the entries belong to the attacker.
|
|
27
|
+
|
|
28
|
+
>>> from blockchainkit.network import eclipse_probability
|
|
29
|
+
>>> eclipse_probability(0.5, 8)
|
|
30
|
+
0.00390625
|
|
31
|
+
"""
|
|
32
|
+
probability(attacker_fraction, "attacker_fraction")
|
|
33
|
+
integer(outbound, "outbound", 1)
|
|
34
|
+
return float(attacker_fraction**outbound)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AddressManager:
|
|
38
|
+
"""A node's table of known peer addresses, with optional group bucketing.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
buckets : int
|
|
43
|
+
Number of buckets.
|
|
44
|
+
bucket_size : int
|
|
45
|
+
Capacity of each bucket. Adding to a full bucket evicts a random
|
|
46
|
+
entry, so a flood of new addresses pushes old ones out.
|
|
47
|
+
buckets_per_group : int, optional
|
|
48
|
+
If given, each network group can place addresses in only this many
|
|
49
|
+
buckets (Bitcoin's defense). If omitted, an address may land in any
|
|
50
|
+
bucket.
|
|
51
|
+
seed : int
|
|
52
|
+
Seeds both the node's secret bucketing key and its random choices.
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> from blockchainkit.network import AddressManager
|
|
57
|
+
>>> table = AddressManager(buckets=4, bucket_size=2, seed=1)
|
|
58
|
+
>>> table.add("10.0.0.1", "10.0")
|
|
59
|
+
>>> table.addresses
|
|
60
|
+
('10.0.0.1',)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
buckets: int = 64,
|
|
67
|
+
bucket_size: int = 16,
|
|
68
|
+
buckets_per_group: int | None = None,
|
|
69
|
+
seed: int = 0,
|
|
70
|
+
) -> None:
|
|
71
|
+
integer(buckets, "buckets", 1)
|
|
72
|
+
integer(bucket_size, "bucket_size", 1)
|
|
73
|
+
if buckets_per_group is not None:
|
|
74
|
+
integer(buckets_per_group, "buckets_per_group", 1)
|
|
75
|
+
check_seed(seed)
|
|
76
|
+
self._random = Random(seed)
|
|
77
|
+
self._secret = self._random.randbytes(16)
|
|
78
|
+
self._per_group = buckets_per_group
|
|
79
|
+
self._buckets: list[list[str]] = [[] for _ in range(buckets)]
|
|
80
|
+
self._size = bucket_size
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return f"AddressManager(buckets={len(self._buckets)}, addresses={len(self.addresses)})"
|
|
84
|
+
|
|
85
|
+
def _hash(self, *parts: str) -> int:
|
|
86
|
+
return int.from_bytes(sha256(self._secret + "\0".join(parts).encode())[:8], "big")
|
|
87
|
+
|
|
88
|
+
def bucket_of(self, address: str, group: str) -> int:
|
|
89
|
+
"""The bucket an address is stored in, derived from the node's secret key."""
|
|
90
|
+
if self._per_group is None:
|
|
91
|
+
return self._hash(address) % len(self._buckets)
|
|
92
|
+
slot = self._hash(address) % self._per_group
|
|
93
|
+
return self._hash(group, str(slot)) % len(self._buckets)
|
|
94
|
+
|
|
95
|
+
def add(self, address: str, group: str) -> None:
|
|
96
|
+
"""Store an address heard from the network, evicting a random entry if needed."""
|
|
97
|
+
if not isinstance(address, str) or not isinstance(group, str):
|
|
98
|
+
raise TypeError("address and group must be str")
|
|
99
|
+
bucket = self._buckets[self.bucket_of(address, group)]
|
|
100
|
+
if address in bucket:
|
|
101
|
+
return
|
|
102
|
+
if len(bucket) >= self._size:
|
|
103
|
+
bucket.pop(self._random.randrange(len(bucket)))
|
|
104
|
+
bucket.append(address)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def addresses(self) -> tuple[str, ...]:
|
|
108
|
+
"""Every stored address, bucket by bucket."""
|
|
109
|
+
return tuple(address for bucket in self._buckets for address in bucket)
|
|
110
|
+
|
|
111
|
+
def select(self, count: int) -> tuple[str, ...]:
|
|
112
|
+
"""Choose ``count`` distinct addresses: each time a random nonempty bucket, then an entry.
|
|
113
|
+
|
|
114
|
+
Picking a bucket first, as Bitcoin does, means an attacker confined
|
|
115
|
+
to a few buckets is picked rarely even if those buckets are full.
|
|
116
|
+
"""
|
|
117
|
+
integer(count, "count", 1)
|
|
118
|
+
if count > len(self.addresses):
|
|
119
|
+
raise ValueError("not enough addresses")
|
|
120
|
+
chosen: list[str] = []
|
|
121
|
+
while len(chosen) < count:
|
|
122
|
+
bucket = self._random.choice([b for b in self._buckets if b])
|
|
123
|
+
address = self._random.choice(bucket)
|
|
124
|
+
if address not in chosen:
|
|
125
|
+
chosen.append(address)
|
|
126
|
+
return tuple(chosen)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Byzantine reliable broadcast (Bracha 1987).
|
|
2
|
+
|
|
3
|
+
A sender wants every correct process to deliver the same value, even if the
|
|
4
|
+
sender lies by telling different processes different things. Bracha's
|
|
5
|
+
protocol adds two all-to-all phases on top of the sender's message:
|
|
6
|
+
|
|
7
|
+
1. **Echo.** On the sender's value, a process tells everyone "I got v".
|
|
8
|
+
2. **Ready.** On ``ceil((n + t + 1) / 2)`` echoes of v, or ``t + 1`` readies
|
|
9
|
+
of v (amplification), a process tells everyone "ready for v", once.
|
|
10
|
+
3. **Deliver.** On ``2t + 1`` readies of v, a process delivers v.
|
|
11
|
+
|
|
12
|
+
Any two echo quorums overlap in a correct process, which echoes only once,
|
|
13
|
+
so correct processes never become ready for different values. Amplification
|
|
14
|
+
makes delivery all-or-nothing. Both arguments need ``n > 3t``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from collections.abc import Iterable, Sequence
|
|
18
|
+
|
|
19
|
+
from blockchainkit._validation import integer
|
|
20
|
+
from blockchainkit.network.core.base import BroadcastResult
|
|
21
|
+
|
|
22
|
+
SENDER = 0
|
|
23
|
+
"""int: The broadcasting process."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def reliable_broadcast(
|
|
27
|
+
n: int,
|
|
28
|
+
faulty: Iterable[int],
|
|
29
|
+
proposals: Sequence[str],
|
|
30
|
+
*,
|
|
31
|
+
tolerance: int | None = None,
|
|
32
|
+
) -> BroadcastResult:
|
|
33
|
+
"""Run Bracha's broadcast in synchronous rounds from process 0.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
n : int
|
|
38
|
+
Number of processes, at least 2.
|
|
39
|
+
faulty : iterable of int
|
|
40
|
+
Byzantine processes. A faulty sender sends ``proposals[r]`` to each
|
|
41
|
+
process ``r``. Faulty processes collude with that split: to each
|
|
42
|
+
correct process ``r`` they echo and ready ``proposals[r]``, the
|
|
43
|
+
strongest support they can give to a disagreement.
|
|
44
|
+
proposals : sequence of str
|
|
45
|
+
One value per process; entries at faulty processes are ignored. If
|
|
46
|
+
the sender is correct, all correct entries must equal ``proposals[0]``.
|
|
47
|
+
tolerance : int, optional
|
|
48
|
+
The ``t`` used in the thresholds; defaults to ``(n - 1) // 3``, the
|
|
49
|
+
most the protocol can tolerate. Running with more faulty processes
|
|
50
|
+
than ``tolerance`` shows the bound is needed.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
BroadcastResult
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
>>> from blockchainkit.network import reliable_broadcast
|
|
59
|
+
>>> result = reliable_broadcast(4, {0}, ["a", "a", "b", "b"])
|
|
60
|
+
>>> result.agreement, result.totality, sorted(set(result.delivered.values()))
|
|
61
|
+
(True, True, ['b'])
|
|
62
|
+
"""
|
|
63
|
+
integer(n, "n", 2)
|
|
64
|
+
bad = set(faulty)
|
|
65
|
+
for process in bad:
|
|
66
|
+
integer(process, "faulty process")
|
|
67
|
+
if process >= n:
|
|
68
|
+
raise ValueError("faulty processes must be in range(n)")
|
|
69
|
+
t = (n - 1) // 3 if tolerance is None else tolerance
|
|
70
|
+
integer(t, "tolerance")
|
|
71
|
+
if len(proposals) != n or not all(isinstance(v, str) for v in proposals):
|
|
72
|
+
raise ValueError("give one str proposal per process")
|
|
73
|
+
correct = [p for p in range(n) if p not in bad]
|
|
74
|
+
if SENDER not in bad and any(proposals[p] != proposals[SENDER] for p in correct):
|
|
75
|
+
raise ValueError("a correct sender sends the same value to everyone")
|
|
76
|
+
echo_quorum, amplify, deliver_quorum = -(-(n + t + 1) // 2), t + 1, 2 * t + 1
|
|
77
|
+
|
|
78
|
+
echoes: dict[int, dict[str, set[int]]] = {p: {} for p in correct}
|
|
79
|
+
readies: dict[int, dict[str, set[int]]] = {p: {} for p in correct}
|
|
80
|
+
for r in correct: # Faulty processes back the value the sender gave r.
|
|
81
|
+
for f in bad:
|
|
82
|
+
echoes[r].setdefault(proposals[r], set()).add(f)
|
|
83
|
+
readies[r].setdefault(proposals[r], set()).add(f)
|
|
84
|
+
echoed: set[int] = set()
|
|
85
|
+
ready: dict[int, str] = {}
|
|
86
|
+
delivered: dict[int, str | None] = dict.fromkeys(correct)
|
|
87
|
+
messages = len(correct) if SENDER in correct else 0 # The SEND messages.
|
|
88
|
+
|
|
89
|
+
def broadcast(table: dict[int, dict[str, set[int]]], source: int, value: str) -> int:
|
|
90
|
+
for r in correct:
|
|
91
|
+
table[r].setdefault(value, set()).add(source)
|
|
92
|
+
return n
|
|
93
|
+
|
|
94
|
+
changed = True
|
|
95
|
+
while changed:
|
|
96
|
+
changed = False
|
|
97
|
+
sends: list[tuple[str, int, str]] = []
|
|
98
|
+
for p in correct:
|
|
99
|
+
if p not in echoed:
|
|
100
|
+
sends.append(("echo", p, proposals[p]))
|
|
101
|
+
echoed.add(p)
|
|
102
|
+
if p not in ready:
|
|
103
|
+
candidates = sorted(
|
|
104
|
+
v
|
|
105
|
+
for v in set(echoes[p]) | set(readies[p])
|
|
106
|
+
if len(echoes[p].get(v, ())) >= echo_quorum
|
|
107
|
+
or len(readies[p].get(v, ())) >= amplify
|
|
108
|
+
)
|
|
109
|
+
if candidates:
|
|
110
|
+
ready[p] = candidates[0]
|
|
111
|
+
sends.append(("ready", p, candidates[0]))
|
|
112
|
+
if delivered[p] is None:
|
|
113
|
+
for v in sorted(readies[p]):
|
|
114
|
+
if len(readies[p][v]) >= deliver_quorum:
|
|
115
|
+
delivered[p] = v
|
|
116
|
+
changed = True
|
|
117
|
+
break
|
|
118
|
+
for kind, p, value in sends: # Messages of a round arrive together.
|
|
119
|
+
messages += broadcast(echoes if kind == "echo" else readies, p, value)
|
|
120
|
+
changed = True
|
|
121
|
+
values = {v for v in delivered.values() if v is not None}
|
|
122
|
+
some = any(v is not None for v in delivered.values())
|
|
123
|
+
every = all(v is not None for v in delivered.values())
|
|
124
|
+
return BroadcastResult(delivered, len(values) <= 1, every or not some, messages)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Logical clocks: Lamport timestamps (1978) and vector clocks (Fidge, Mattern 1988).
|
|
2
|
+
|
|
3
|
+
Peers have no shared clock, so "which happened first?" has no physical
|
|
4
|
+
answer. Lamport defined *happened before* from the messages themselves: an
|
|
5
|
+
event precedes later events at the same process, a send precedes its
|
|
6
|
+
receive, and the relation is transitive. Two events related neither way are
|
|
7
|
+
*concurrent*.
|
|
8
|
+
|
|
9
|
+
A history is a sequence of processes, each a sequence of events. An event is
|
|
10
|
+
a pair ``(kind, label)``: ``("local", name)``, ``("send", message)`` or
|
|
11
|
+
``("receive", message)``. Each message label is sent once and received at
|
|
12
|
+
most once.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
Event = tuple[str, str]
|
|
18
|
+
"""``(kind, label)`` with kind ``"local"``, ``"send"`` or ``"receive"``."""
|
|
19
|
+
|
|
20
|
+
_KINDS = ("local", "send", "receive")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _schedule(processes: Sequence[Sequence[Event]]) -> list[tuple[int, int]]:
|
|
24
|
+
"""Positions ``(process, index)`` in an order where every send precedes its receive."""
|
|
25
|
+
senders: dict[str, tuple[int, int]] = {}
|
|
26
|
+
received: set[str] = set()
|
|
27
|
+
for p, events in enumerate(processes):
|
|
28
|
+
for i, event in enumerate(events):
|
|
29
|
+
if not isinstance(event, tuple) or len(event) != 2 or event[0] not in _KINDS:
|
|
30
|
+
raise ValueError(f"event {event!r} is not (kind, label) with a known kind")
|
|
31
|
+
kind, label = event
|
|
32
|
+
if kind == "send":
|
|
33
|
+
if label in senders:
|
|
34
|
+
raise ValueError(f"message {label!r} is sent twice")
|
|
35
|
+
senders[label] = (p, i)
|
|
36
|
+
elif kind == "receive":
|
|
37
|
+
if label in received:
|
|
38
|
+
raise ValueError(f"message {label!r} is received twice")
|
|
39
|
+
received.add(label)
|
|
40
|
+
if missing := received - senders.keys():
|
|
41
|
+
raise ValueError(f"messages received but never sent: {sorted(missing)}")
|
|
42
|
+
done: set[tuple[int, int]] = set()
|
|
43
|
+
order: list[tuple[int, int]] = []
|
|
44
|
+
cursor = [0] * len(processes)
|
|
45
|
+
progress = True
|
|
46
|
+
while progress:
|
|
47
|
+
progress = False
|
|
48
|
+
for p, events in enumerate(processes):
|
|
49
|
+
while cursor[p] < len(events):
|
|
50
|
+
kind, label = events[cursor[p]]
|
|
51
|
+
if kind == "receive" and senders[label] not in done:
|
|
52
|
+
break # Wait until the matching send has a timestamp.
|
|
53
|
+
done.add((p, cursor[p]))
|
|
54
|
+
order.append((p, cursor[p]))
|
|
55
|
+
cursor[p] += 1
|
|
56
|
+
progress = True
|
|
57
|
+
if len(order) != sum(len(events) for events in processes):
|
|
58
|
+
raise ValueError("the history has a causal cycle: a receive waits on its own future")
|
|
59
|
+
return order
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def lamport_timestamps(processes: Sequence[Sequence[Event]]) -> tuple[tuple[int, ...], ...]:
|
|
63
|
+
"""Assign Lamport clock values to every event.
|
|
64
|
+
|
|
65
|
+
Each process keeps a counter. It increments the counter before each
|
|
66
|
+
event, attaches it to every message it sends, and on receipt jumps to
|
|
67
|
+
``max(own, received) + 1``. The result satisfies the *clock condition*:
|
|
68
|
+
if a happened before b, then ``C(a) < C(b)``. The converse does not hold.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
tuple of tuple of int
|
|
73
|
+
``result[p][i]`` is the timestamp of event ``i`` at process ``p``.
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> from blockchainkit.network import lamport_timestamps
|
|
78
|
+
>>> lamport_timestamps([[("send", "m")], [("local", "x"), ("local", "y"), ("receive", "m")]])
|
|
79
|
+
((1,), (1, 2, 3))
|
|
80
|
+
"""
|
|
81
|
+
stamps = [[0] * len(events) for events in processes]
|
|
82
|
+
clock = [0] * len(processes)
|
|
83
|
+
sent: dict[str, int] = {}
|
|
84
|
+
for p, i in _schedule(processes):
|
|
85
|
+
kind, label = processes[p][i]
|
|
86
|
+
if kind == "receive":
|
|
87
|
+
clock[p] = max(clock[p], sent[label])
|
|
88
|
+
clock[p] += 1
|
|
89
|
+
stamps[p][i] = clock[p]
|
|
90
|
+
if kind == "send":
|
|
91
|
+
sent[label] = clock[p]
|
|
92
|
+
return tuple(tuple(row) for row in stamps)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def vector_timestamps(
|
|
96
|
+
processes: Sequence[Sequence[Event]],
|
|
97
|
+
) -> tuple[tuple[tuple[int, ...], ...], ...]:
|
|
98
|
+
"""Assign vector clock values to every event.
|
|
99
|
+
|
|
100
|
+
Each process ``p`` keeps one counter per process. It increments entry
|
|
101
|
+
``p`` before each event and, on receipt, first takes the entrywise maximum
|
|
102
|
+
with the vector carried by the message. Entry ``q`` of an event's vector
|
|
103
|
+
counts the events at ``q`` that happened before or at it, so vectors
|
|
104
|
+
characterize causality exactly: see :func:`happened_before`.
|
|
105
|
+
|
|
106
|
+
Examples
|
|
107
|
+
--------
|
|
108
|
+
>>> from blockchainkit.network import vector_timestamps
|
|
109
|
+
>>> vector_timestamps([[("send", "m")], [("local", "x"), ("receive", "m")]])
|
|
110
|
+
(((1, 0),), ((0, 1), (1, 2)))
|
|
111
|
+
"""
|
|
112
|
+
n = len(processes)
|
|
113
|
+
stamps: list[list[tuple[int, ...]]] = [[()] * len(events) for events in processes]
|
|
114
|
+
clock = [[0] * n for _ in range(n)]
|
|
115
|
+
sent: dict[str, tuple[int, ...]] = {}
|
|
116
|
+
for p, i in _schedule(processes):
|
|
117
|
+
kind, label = processes[p][i]
|
|
118
|
+
if kind == "receive":
|
|
119
|
+
clock[p] = [max(a, b) for a, b in zip(clock[p], sent[label], strict=True)]
|
|
120
|
+
clock[p][p] += 1
|
|
121
|
+
stamps[p][i] = tuple(clock[p])
|
|
122
|
+
if kind == "send":
|
|
123
|
+
sent[label] = stamps[p][i]
|
|
124
|
+
return tuple(tuple(row) for row in stamps)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def happened_before(a: Sequence[int], b: Sequence[int]) -> bool:
|
|
128
|
+
"""True if the event stamped ``a`` happened before the event stamped ``b``.
|
|
129
|
+
|
|
130
|
+
For vector timestamps, ``a -> b`` exactly when ``a <= b`` entrywise and
|
|
131
|
+
``a != b``.
|
|
132
|
+
|
|
133
|
+
>>> from blockchainkit.network import happened_before
|
|
134
|
+
>>> happened_before((1, 0), (1, 2)), happened_before((1, 0), (0, 1))
|
|
135
|
+
(True, False)
|
|
136
|
+
"""
|
|
137
|
+
if len(a) != len(b):
|
|
138
|
+
raise ValueError("vector timestamps must have the same length")
|
|
139
|
+
return all(x <= y for x, y in zip(a, b, strict=True)) and tuple(a) != tuple(b)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def concurrent(a: Sequence[int], b: Sequence[int]) -> bool:
|
|
143
|
+
"""True if neither vector-stamped event happened before the other (and they differ).
|
|
144
|
+
|
|
145
|
+
>>> from blockchainkit.network import concurrent
|
|
146
|
+
>>> concurrent((1, 0), (0, 1))
|
|
147
|
+
True
|
|
148
|
+
"""
|
|
149
|
+
return tuple(a) != tuple(b) and not happened_before(a, b) and not happened_before(b, a)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Epidemic dissemination: push, pull and push-pull rumor spreading.
|
|
2
|
+
|
|
3
|
+
Each round, every peer calls one neighbor chosen uniformly at random. In
|
|
4
|
+
*push*, informed callers tell the callee; in *pull*, uninformed callers ask
|
|
5
|
+
the callee and learn the rumor if it knows it; *push-pull* does both. Demers
|
|
6
|
+
et al. (1987) compared these for replicated databases. On the complete graph,
|
|
7
|
+
push alone informs everyone in about ``log2 n + ln n`` rounds (Frieze and
|
|
8
|
+
Grimmett 1985, Pittel 1987).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from math import log, log2
|
|
12
|
+
from random import Random
|
|
13
|
+
|
|
14
|
+
from blockchainkit._validation import integer
|
|
15
|
+
from blockchainkit._validation import seed as check_seed
|
|
16
|
+
from blockchainkit.network.core.base import RumorRun
|
|
17
|
+
from blockchainkit.network.systems.topology import Graph
|
|
18
|
+
|
|
19
|
+
MODES = ("push", "pull", "push-pull")
|
|
20
|
+
"""tuple of str: The supported exchange modes."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def pittel_rounds(n: int) -> float:
|
|
24
|
+
"""Rounds push gossip needs on the complete graph: ``log2 n + ln n``, up to O(1).
|
|
25
|
+
|
|
26
|
+
The ``log2 n`` term is the doubling phase, while few peers know the
|
|
27
|
+
rumor; the ``ln n`` term is the coupon-collector tail, while the last
|
|
28
|
+
uninformed peers wait to be called.
|
|
29
|
+
|
|
30
|
+
>>> from blockchainkit.network import pittel_rounds
|
|
31
|
+
>>> round(pittel_rounds(1024), 2)
|
|
32
|
+
16.93
|
|
33
|
+
"""
|
|
34
|
+
integer(n, "n", 1)
|
|
35
|
+
return log2(n) + log(n)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def spread_rumor(
|
|
39
|
+
network: Graph | int,
|
|
40
|
+
*,
|
|
41
|
+
mode: str = "push",
|
|
42
|
+
source: int = 0,
|
|
43
|
+
seed: int = 0,
|
|
44
|
+
max_rounds: int = 10_000,
|
|
45
|
+
) -> RumorRun:
|
|
46
|
+
"""Simulate synchronous random-call rumor spreading.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
network : Graph or int
|
|
51
|
+
The peer graph, or an integer ``n`` for the complete graph on ``n``
|
|
52
|
+
peers (simulated without building its n(n-1)/2 edges).
|
|
53
|
+
mode : {"push", "pull", "push-pull"}
|
|
54
|
+
Who learns from each call; see the module docstring.
|
|
55
|
+
source : int
|
|
56
|
+
The peer that knows the rumor at round 0.
|
|
57
|
+
seed : int
|
|
58
|
+
Seed for the random calls.
|
|
59
|
+
max_rounds : int
|
|
60
|
+
Stop after this many rounds even if peers remain uninformed.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
RumorRun
|
|
65
|
+
Informed counts per round. The run also stops once every peer that
|
|
66
|
+
the source can reach is informed.
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> from blockchainkit.network import spread_rumor
|
|
71
|
+
>>> run = spread_rumor(1000, seed=1)
|
|
72
|
+
>>> run.complete, run.informed[:4]
|
|
73
|
+
(True, (1, 2, 4, 8))
|
|
74
|
+
"""
|
|
75
|
+
if mode not in MODES:
|
|
76
|
+
raise ValueError(f"mode must be one of {MODES}")
|
|
77
|
+
check_seed(seed)
|
|
78
|
+
integer(max_rounds, "max_rounds", 1)
|
|
79
|
+
if isinstance(network, Graph):
|
|
80
|
+
n = network.n
|
|
81
|
+
reachable = sum(1 for d in network.distances(source) if d is not None)
|
|
82
|
+
else:
|
|
83
|
+
integer(network, "n", 1)
|
|
84
|
+
n = reachable = network
|
|
85
|
+
integer(source, "source")
|
|
86
|
+
if source >= n:
|
|
87
|
+
raise ValueError("source must be a peer in range(n)")
|
|
88
|
+
random = Random(seed)
|
|
89
|
+
|
|
90
|
+
def call(peer: int) -> int | None:
|
|
91
|
+
if isinstance(network, Graph):
|
|
92
|
+
neighbors = network.neighbors(peer)
|
|
93
|
+
return random.choice(neighbors) if neighbors else None
|
|
94
|
+
partner = random.randrange(n - 1) # Any peer but the caller.
|
|
95
|
+
return partner + 1 if partner >= peer else partner
|
|
96
|
+
|
|
97
|
+
informed = [False] * n
|
|
98
|
+
informed[source] = True
|
|
99
|
+
counts = [1]
|
|
100
|
+
while counts[-1] < reachable and len(counts) <= max_rounds:
|
|
101
|
+
before = informed[:] # Calls in a round see the state at its start.
|
|
102
|
+
for caller in range(n):
|
|
103
|
+
callee = call(caller)
|
|
104
|
+
if callee is None:
|
|
105
|
+
continue
|
|
106
|
+
if mode != "pull" and before[caller]:
|
|
107
|
+
informed[callee] = True
|
|
108
|
+
if mode != "push" and before[callee]:
|
|
109
|
+
informed[caller] = True
|
|
110
|
+
counts.append(sum(informed))
|
|
111
|
+
return RumorRun(tuple(counts), n)
|