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,232 @@
|
|
|
1
|
+
"""Educational Schnorr signatures and interactive proof transcripts.
|
|
2
|
+
|
|
3
|
+
This is a domain-separated teaching scheme, not Bitcoin BIP-340. Python
|
|
4
|
+
curve arithmetic is variable-time. Explicit nonces exist for experiments;
|
|
5
|
+
reusing a nonce reveals the private key.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import secrets
|
|
11
|
+
|
|
12
|
+
from blockchainkit._validation import integer
|
|
13
|
+
from blockchainkit.constants import SCHNORR_DOMAIN
|
|
14
|
+
from blockchainkit.crypto.core.base import SchnorrSignature
|
|
15
|
+
from blockchainkit.crypto.systems.curves import (
|
|
16
|
+
SECP256K1,
|
|
17
|
+
Curve,
|
|
18
|
+
add,
|
|
19
|
+
encode_point,
|
|
20
|
+
multiply,
|
|
21
|
+
public_key,
|
|
22
|
+
)
|
|
23
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def challenge(
|
|
27
|
+
message: bytes,
|
|
28
|
+
commitment: tuple[int, int],
|
|
29
|
+
public: tuple[int, int],
|
|
30
|
+
curve: Curve = SECP256K1,
|
|
31
|
+
) -> int:
|
|
32
|
+
"""Hash the curve parameters, public key, commitment, and message to a scalar."""
|
|
33
|
+
if not isinstance(message, bytes):
|
|
34
|
+
raise TypeError("message must be bytes")
|
|
35
|
+
domain = f"{SCHNORR_DOMAIN}:{curve.p}:{curve.a}:{curve.b}:{curve.order}:".encode()
|
|
36
|
+
framed = domain + encode_point(curve.generator, curve)
|
|
37
|
+
framed += encode_point(commitment, curve) + encode_point(public, curve) + message
|
|
38
|
+
return int.from_bytes(sha256(framed), "big") % curve.order
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sign(
|
|
42
|
+
message: bytes,
|
|
43
|
+
private: int,
|
|
44
|
+
*,
|
|
45
|
+
nonce: int | None = None,
|
|
46
|
+
curve: Curve = SECP256K1,
|
|
47
|
+
) -> SchnorrSignature:
|
|
48
|
+
"""Sign with s = k + H(R, Q, m)*x modulo the subgroup order.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
message : bytes
|
|
53
|
+
Exact bytes to authenticate.
|
|
54
|
+
private : int
|
|
55
|
+
Secret scalar x.
|
|
56
|
+
nonce : int, optional
|
|
57
|
+
Experiment-only scalar k. Defaults to system randomness.
|
|
58
|
+
curve : Curve
|
|
59
|
+
Defaults to secp256k1; tiny curves illustrate algebra, not security.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
SchnorrSignature
|
|
64
|
+
Commitment R=kG and response s.
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
>>> from blockchainkit.crypto import sign, verify, public_key
|
|
69
|
+
>>> signature = sign(b"lesson", 7, nonce=11)
|
|
70
|
+
>>> verify(b"lesson", signature, public_key(7))
|
|
71
|
+
True
|
|
72
|
+
"""
|
|
73
|
+
public = public_key(private, curve)
|
|
74
|
+
if nonce is None:
|
|
75
|
+
nonce = secrets.randbelow(curve.order - 1) + 1
|
|
76
|
+
commitment = public_key(nonce, curve)
|
|
77
|
+
c = challenge(message, commitment, public, curve)
|
|
78
|
+
return SchnorrSignature(commitment, (nonce + c * private) % curve.order)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def verify_transcript(
|
|
82
|
+
public: tuple[int, int],
|
|
83
|
+
commitment: tuple[int, int],
|
|
84
|
+
challenge_scalar: int,
|
|
85
|
+
response: int,
|
|
86
|
+
curve: Curve = SECP256K1,
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""Check the interactive Schnorr equation sG = R + cQ.
|
|
89
|
+
|
|
90
|
+
An accepting transcript is not itself proof of a live interaction:
|
|
91
|
+
choosing c and s first permits simulation via R=sG-cQ. Soundness needs
|
|
92
|
+
an unpredictable verifier challenge after the prover commits to R.
|
|
93
|
+
"""
|
|
94
|
+
for scalar in (challenge_scalar, response):
|
|
95
|
+
if type(scalar) is not int or not 0 <= scalar < curve.order:
|
|
96
|
+
return False
|
|
97
|
+
for point in (public, commitment):
|
|
98
|
+
if point is None or not curve.contains(point):
|
|
99
|
+
return False
|
|
100
|
+
# With cofactor 1 every curve point is in the subgroup (see Curve.cofactor_is_one).
|
|
101
|
+
if not curve.cofactor_is_one and multiply(curve.order, point, curve) is not None:
|
|
102
|
+
return False
|
|
103
|
+
return multiply(response, curve.generator, curve) == add(
|
|
104
|
+
commitment, multiply(challenge_scalar, public, curve), curve
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def simulate_transcript(
|
|
109
|
+
public: tuple[int, int],
|
|
110
|
+
challenge_scalar: int,
|
|
111
|
+
response: int,
|
|
112
|
+
curve: Curve = SECP256K1,
|
|
113
|
+
) -> tuple[int, int]:
|
|
114
|
+
"""Forge an accepting transcript *without* the private key: R = sG - cQ.
|
|
115
|
+
|
|
116
|
+
Choosing the challenge and response first, then solving for the
|
|
117
|
+
commitment, produces ``(R, c, s)`` that :func:`verify_transcript` accepts.
|
|
118
|
+
This is the simulator in the zero-knowledge proof (Goldwasser, Micali and
|
|
119
|
+
Rackoff, 1985): real transcripts and simulated ones look the same, so a
|
|
120
|
+
transcript teaches the verifier nothing. A live proof is sound only
|
|
121
|
+
because the verifier picks c *after* seeing R.
|
|
122
|
+
"""
|
|
123
|
+
for scalar in (challenge_scalar, response):
|
|
124
|
+
integer(scalar, "scalar")
|
|
125
|
+
if scalar >= curve.order:
|
|
126
|
+
raise ValueError("scalars must be below the curve order")
|
|
127
|
+
negated = multiply(-challenge_scalar, public, curve)
|
|
128
|
+
commitment = add(multiply(response, curve.generator, curve), negated, curve)
|
|
129
|
+
if commitment is None:
|
|
130
|
+
raise ValueError("these scalars give the point at infinity; choose others")
|
|
131
|
+
return commitment
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def deterministic_nonce(private: int, message: bytes, order: int = SECP256K1.order) -> int:
|
|
135
|
+
"""Derive a signing nonce from the key and message (RFC 6979, HMAC-SHA256).
|
|
136
|
+
|
|
137
|
+
Random nonces fail when the randomness does: a repeated or predictable
|
|
138
|
+
nonce reveals the private key (see :func:`recover_reused_nonce_key`).
|
|
139
|
+
RFC 6979 removes the random number generator from signing: the nonce is
|
|
140
|
+
an HMAC-DRBG output seeded with the private key and the message hash, so
|
|
141
|
+
it is unpredictable without the key, and distinct messages get distinct
|
|
142
|
+
nonces. This implements section 3.2 with SHA-256, and reproduces the
|
|
143
|
+
RFC's published nonces when given the same order.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
private : int
|
|
148
|
+
Secret scalar in [1, order).
|
|
149
|
+
message : bytes
|
|
150
|
+
The message to be signed (it is hashed here).
|
|
151
|
+
order : int
|
|
152
|
+
The group order q; defaults to secp256k1's.
|
|
153
|
+
|
|
154
|
+
Examples
|
|
155
|
+
--------
|
|
156
|
+
>>> from blockchainkit.crypto import deterministic_nonce, sign, verify, public_key
|
|
157
|
+
>>> k = deterministic_nonce(7, b"lesson")
|
|
158
|
+
>>> verify(b"lesson", sign(b"lesson", 7, nonce=k), public_key(7))
|
|
159
|
+
True
|
|
160
|
+
"""
|
|
161
|
+
integer(order, "order", 2)
|
|
162
|
+
integer(private, "private", 1)
|
|
163
|
+
if private >= order:
|
|
164
|
+
raise ValueError("private key must be below the order")
|
|
165
|
+
if not isinstance(message, bytes):
|
|
166
|
+
raise TypeError("message must be bytes")
|
|
167
|
+
qlen = order.bit_length()
|
|
168
|
+
rlen = (qlen + 7) // 8
|
|
169
|
+
|
|
170
|
+
def bits2int(data: bytes) -> int:
|
|
171
|
+
value = int.from_bytes(data, "big")
|
|
172
|
+
excess = 8 * len(data) - qlen
|
|
173
|
+
return value >> excess if excess > 0 else value
|
|
174
|
+
|
|
175
|
+
def mac(key: bytes, data: bytes) -> bytes:
|
|
176
|
+
return hmac.new(key, data, hashlib.sha256).digest()
|
|
177
|
+
|
|
178
|
+
seed = private.to_bytes(rlen, "big") + (bits2int(sha256(message)) % order).to_bytes(rlen, "big")
|
|
179
|
+
v, k = b"\x01" * 32, b"\x00" * 32
|
|
180
|
+
k = mac(k, v + b"\x00" + seed)
|
|
181
|
+
v = mac(k, v)
|
|
182
|
+
k = mac(k, v + b"\x01" + seed)
|
|
183
|
+
v = mac(k, v)
|
|
184
|
+
while True:
|
|
185
|
+
t = b""
|
|
186
|
+
while 8 * len(t) < qlen:
|
|
187
|
+
v = mac(k, v)
|
|
188
|
+
t += v
|
|
189
|
+
nonce = bits2int(t)
|
|
190
|
+
if 1 <= nonce < order:
|
|
191
|
+
return nonce
|
|
192
|
+
k = mac(k, v + b"\x00")
|
|
193
|
+
v = mac(k, v)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def verify(
|
|
197
|
+
message: bytes,
|
|
198
|
+
signature: SchnorrSignature,
|
|
199
|
+
public: tuple[int, int],
|
|
200
|
+
curve: Curve = SECP256K1,
|
|
201
|
+
) -> bool:
|
|
202
|
+
"""Verify a Schnorr signature; malformed points or scalars return False."""
|
|
203
|
+
try:
|
|
204
|
+
c = challenge(message, signature.commitment, public, curve)
|
|
205
|
+
return verify_transcript(public, signature.commitment, c, signature.response, curve)
|
|
206
|
+
except (ValueError, TypeError, AttributeError, OverflowError):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def recover_reused_nonce_key(
|
|
211
|
+
first: SchnorrSignature,
|
|
212
|
+
second: SchnorrSignature,
|
|
213
|
+
first_challenge: int,
|
|
214
|
+
second_challenge: int,
|
|
215
|
+
order: int = SECP256K1.order,
|
|
216
|
+
) -> int:
|
|
217
|
+
"""Recover x=(s1-s2)/(c1-c2) when two signatures reuse their nonce.
|
|
218
|
+
|
|
219
|
+
This demonstrates special soundness and why nonce reuse is catastrophic.
|
|
220
|
+
The caller supplies the actual challenges derived from the two messages.
|
|
221
|
+
"""
|
|
222
|
+
integer(order, "order", 2)
|
|
223
|
+
if first.commitment != second.commitment:
|
|
224
|
+
raise ValueError("commitments must be identical")
|
|
225
|
+
for scalar in (first.response, second.response, first_challenge, second_challenge):
|
|
226
|
+
integer(scalar, "scalar")
|
|
227
|
+
if scalar >= order:
|
|
228
|
+
raise ValueError("scalar must be below order")
|
|
229
|
+
delta = (first_challenge - second_challenge) % order
|
|
230
|
+
if delta == 0:
|
|
231
|
+
raise ValueError("challenges must differ modulo order")
|
|
232
|
+
return (first.response - second.response) * pow(delta, -1, order) % order
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Deterministic primality testing for the bounded parameters used in experiments."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_prime(value: int) -> bool:
|
|
5
|
+
"""Test primality deterministically for integers below 2**64.
|
|
6
|
+
|
|
7
|
+
Uses trial division followed by deterministic Miller--Rabin bases for
|
|
8
|
+
this bounded domain. Larger numbers are deliberately not accepted.
|
|
9
|
+
|
|
10
|
+
>>> from blockchainkit.crypto.utils import is_prime
|
|
11
|
+
>>> is_prime(7919), is_prime(561)
|
|
12
|
+
(True, False)
|
|
13
|
+
"""
|
|
14
|
+
if type(value) is not int:
|
|
15
|
+
raise TypeError(f"expected an integer, not {type(value).__name__}")
|
|
16
|
+
if value >= 2**64:
|
|
17
|
+
raise ValueError("expected an integer below 2**64")
|
|
18
|
+
if value < 2:
|
|
19
|
+
return False
|
|
20
|
+
for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
|
|
21
|
+
if value % p == 0:
|
|
22
|
+
return value == p
|
|
23
|
+
d, s = value - 1, 0
|
|
24
|
+
while d % 2 == 0:
|
|
25
|
+
d //= 2
|
|
26
|
+
s += 1
|
|
27
|
+
for base in (2, 325, 9375, 28178, 450775, 9780504, 1795265022):
|
|
28
|
+
if base % value == 0:
|
|
29
|
+
continue
|
|
30
|
+
x = pow(base, d, value)
|
|
31
|
+
if x in (1, value - 1):
|
|
32
|
+
continue
|
|
33
|
+
for _ in range(s - 1):
|
|
34
|
+
x = pow(x, 2, value)
|
|
35
|
+
if x == value - 1:
|
|
36
|
+
break
|
|
37
|
+
else:
|
|
38
|
+
return False
|
|
39
|
+
return True
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Plotting helpers for blockchainkit.crypto.
|
|
2
|
+
|
|
3
|
+
Imports matplotlib, so ``import blockchainkit`` does not load this module;
|
|
4
|
+
import it explicitly: ``from blockchainkit.crypto.visualizers import ...``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from blockchainkit.crypto.visualizers.plots import plot_curve_points, plot_hamming_distances
|
|
8
|
+
|
|
9
|
+
__all__ = ["plot_curve_points", "plot_hamming_distances"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Plotting helpers for blockchainkit.crypto: small curve groups and hash avalanche."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from math import comb
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
from matplotlib.axes import Axes
|
|
11
|
+
|
|
12
|
+
from blockchainkit.crypto.systems.curves import Curve, enumerate_points, multiply
|
|
13
|
+
|
|
14
|
+
__all__ = ["plot_curve_points", "plot_hamming_distances"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plot_curve_points(
|
|
18
|
+
curve: Curve, *, label_multiples: bool = True, ax: Axes | None = None
|
|
19
|
+
) -> Axes:
|
|
20
|
+
"""Scatter every finite point of a small curve and label the multiples kG.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
curve : Curve
|
|
25
|
+
A curve with p <= 10,000
|
|
26
|
+
(see :func:`~blockchainkit.crypto.systems.curves.enumerate_points`).
|
|
27
|
+
label_multiples : bool
|
|
28
|
+
Annotate each point kG of the generator's subgroup with its k, which
|
|
29
|
+
shows that scalar multiplication jumps around the plane with no
|
|
30
|
+
visible pattern: the discrete-log problem.
|
|
31
|
+
ax : matplotlib.axes.Axes, optional
|
|
32
|
+
Axes to draw on; a new figure is created if omitted.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
matplotlib.axes.Axes
|
|
37
|
+
"""
|
|
38
|
+
if ax is None:
|
|
39
|
+
_, ax = plt.subplots()
|
|
40
|
+
points = np.array(enumerate_points(curve))
|
|
41
|
+
ax.scatter(points[:, 0], points[:, 1], s=40, color="#94a3b8", label="curve points")
|
|
42
|
+
if label_multiples:
|
|
43
|
+
for k in range(1, curve.order):
|
|
44
|
+
point = multiply(k, curve.generator, curve)
|
|
45
|
+
assert point is not None # k < order, so kG is finite.
|
|
46
|
+
ax.scatter(*point, s=55, color="#2563eb")
|
|
47
|
+
ax.annotate(f"{k}G", point, xytext=(5, 5), textcoords="offset points", fontsize=8)
|
|
48
|
+
ax.set_xlim(-1, curve.p)
|
|
49
|
+
ax.set_ylim(-1, curve.p)
|
|
50
|
+
ax.set_xlabel("x")
|
|
51
|
+
ax.set_ylabel("y")
|
|
52
|
+
ax.set_title(f"y² = x³ + {curve.a}x + {curve.b} over F_{curve.p}: {len(points) + 1} points")
|
|
53
|
+
return ax
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def plot_hamming_distances(
|
|
57
|
+
distances: Sequence[int], bits: int = 256, ax: Axes | None = None
|
|
58
|
+
) -> Axes:
|
|
59
|
+
"""Histogram of output-bit differences against the ideal Binomial(bits, 1/2).
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
distances : sequence of int
|
|
64
|
+
Hamming distances between digests of related inputs, e.g. from
|
|
65
|
+
:func:`~blockchainkit.crypto.systems.hashing.hamming_distance` after a one-bit flip.
|
|
66
|
+
bits : int
|
|
67
|
+
Digest length in bits.
|
|
68
|
+
ax : matplotlib.axes.Axes, optional
|
|
69
|
+
Axes to draw on; a new figure is created if omitted.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
matplotlib.axes.Axes
|
|
74
|
+
"""
|
|
75
|
+
if ax is None:
|
|
76
|
+
_, ax = plt.subplots()
|
|
77
|
+
values = np.asarray(distances)
|
|
78
|
+
ax.hist(values, bins=18, density=True, color="#0d9488", edgecolor="white", label="observed")
|
|
79
|
+
spread = 4 * np.sqrt(bits) / 2
|
|
80
|
+
ks = np.arange(int(bits / 2 - spread), int(bits / 2 + spread) + 1)
|
|
81
|
+
ideal = np.array([comb(bits, int(k)) for k in ks], dtype=float) / 2.0**bits
|
|
82
|
+
ax.plot(ks, ideal, color="black", label=f"Binomial({bits}, 1/2)")
|
|
83
|
+
ax.axvline(bits / 2, color="black", linestyle="--", linewidth=1)
|
|
84
|
+
ax.set_xlabel("differing output bits")
|
|
85
|
+
ax.set_ylabel("probability")
|
|
86
|
+
ax.set_title("Avalanche: about half the output bits change")
|
|
87
|
+
ax.legend()
|
|
88
|
+
return ax
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Peer-to-peer networks: topologies, logical time, dissemination, overlays, and block relay."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.network.core.base import (
|
|
4
|
+
BroadcastResult,
|
|
5
|
+
CompactBlockResult,
|
|
6
|
+
Delivery,
|
|
7
|
+
LookupResult,
|
|
8
|
+
Operation,
|
|
9
|
+
RelayCost,
|
|
10
|
+
RumorRun,
|
|
11
|
+
)
|
|
12
|
+
from blockchainkit.network.systems.addresses import AddressManager, eclipse_probability
|
|
13
|
+
from blockchainkit.network.systems.broadcast import reliable_broadcast
|
|
14
|
+
from blockchainkit.network.systems.clocks import (
|
|
15
|
+
concurrent,
|
|
16
|
+
happened_before,
|
|
17
|
+
lamport_timestamps,
|
|
18
|
+
vector_timestamps,
|
|
19
|
+
)
|
|
20
|
+
from blockchainkit.network.systems.epidemics import pittel_rounds, spread_rumor
|
|
21
|
+
from blockchainkit.network.systems.gossip import SimulatedNetwork
|
|
22
|
+
from blockchainkit.network.systems.kademlia import KademliaNetwork, node_id, xor_distance
|
|
23
|
+
from blockchainkit.network.systems.privacy import first_spy_precision
|
|
24
|
+
from blockchainkit.network.systems.propagation import fork_rate, simulate_fork_rate
|
|
25
|
+
from blockchainkit.network.systems.relay import compact_block_relay, relay_cost, short_id
|
|
26
|
+
from blockchainkit.network.systems.replication import ReplicatedRegister
|
|
27
|
+
from blockchainkit.network.systems.topology import (
|
|
28
|
+
Graph,
|
|
29
|
+
barabasi_albert,
|
|
30
|
+
complete_graph,
|
|
31
|
+
erdos_renyi,
|
|
32
|
+
ring_lattice,
|
|
33
|
+
watts_strogatz,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"BroadcastResult",
|
|
38
|
+
"CompactBlockResult",
|
|
39
|
+
"Delivery",
|
|
40
|
+
"LookupResult",
|
|
41
|
+
"Operation",
|
|
42
|
+
"RelayCost",
|
|
43
|
+
"RumorRun",
|
|
44
|
+
"AddressManager",
|
|
45
|
+
"eclipse_probability",
|
|
46
|
+
"reliable_broadcast",
|
|
47
|
+
"concurrent",
|
|
48
|
+
"happened_before",
|
|
49
|
+
"lamport_timestamps",
|
|
50
|
+
"vector_timestamps",
|
|
51
|
+
"pittel_rounds",
|
|
52
|
+
"spread_rumor",
|
|
53
|
+
"SimulatedNetwork",
|
|
54
|
+
"KademliaNetwork",
|
|
55
|
+
"node_id",
|
|
56
|
+
"xor_distance",
|
|
57
|
+
"first_spy_precision",
|
|
58
|
+
"fork_rate",
|
|
59
|
+
"simulate_fork_rate",
|
|
60
|
+
"compact_block_relay",
|
|
61
|
+
"relay_cost",
|
|
62
|
+
"short_id",
|
|
63
|
+
"ReplicatedRegister",
|
|
64
|
+
"Graph",
|
|
65
|
+
"barabasi_albert",
|
|
66
|
+
"complete_graph",
|
|
67
|
+
"erdos_renyi",
|
|
68
|
+
"ring_lattice",
|
|
69
|
+
"watts_strogatz",
|
|
70
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Event records and result containers."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.network.core.base import (
|
|
4
|
+
BroadcastResult,
|
|
5
|
+
CompactBlockResult,
|
|
6
|
+
Delivery,
|
|
7
|
+
LookupResult,
|
|
8
|
+
Operation,
|
|
9
|
+
RelayCost,
|
|
10
|
+
RumorRun,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BroadcastResult",
|
|
15
|
+
"CompactBlockResult",
|
|
16
|
+
"Delivery",
|
|
17
|
+
"LookupResult",
|
|
18
|
+
"Operation",
|
|
19
|
+
"RelayCost",
|
|
20
|
+
"RumorRun",
|
|
21
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Event records and result containers for blockchainkit.network."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Delivery:
|
|
8
|
+
"""First receipt of a byte payload at a peer at integer simulation time."""
|
|
9
|
+
|
|
10
|
+
time: int
|
|
11
|
+
sender: str
|
|
12
|
+
recipient: str
|
|
13
|
+
payload: bytes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class RumorRun:
|
|
18
|
+
"""How a rumor spread, round by synchronous round.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
informed : tuple of int
|
|
23
|
+
``informed[r]`` peers knew the rumor after round ``r``;
|
|
24
|
+
``informed[0]`` is 1, the source.
|
|
25
|
+
n : int
|
|
26
|
+
Number of peers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
informed: tuple[int, ...]
|
|
30
|
+
n: int
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def rounds(self) -> int:
|
|
34
|
+
"""Rounds played until the run stopped."""
|
|
35
|
+
return len(self.informed) - 1
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def complete(self) -> bool:
|
|
39
|
+
"""True if every peer heard the rumor."""
|
|
40
|
+
return self.informed[-1] == self.n
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class BroadcastResult:
|
|
45
|
+
"""Outcome of a Byzantine reliable-broadcast run.
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
delivered : dict
|
|
50
|
+
Each correct process's delivered value, or None if it delivered nothing.
|
|
51
|
+
agreement : bool
|
|
52
|
+
No two correct processes delivered different values.
|
|
53
|
+
totality : bool
|
|
54
|
+
Either every correct process delivered or none did.
|
|
55
|
+
messages : int
|
|
56
|
+
Messages sent by correct processes.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
delivered: dict[int, str | None]
|
|
60
|
+
agreement: bool
|
|
61
|
+
totality: bool
|
|
62
|
+
messages: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Operation:
|
|
67
|
+
"""One client request to a replicated register and its outcome.
|
|
68
|
+
|
|
69
|
+
Attributes
|
|
70
|
+
----------
|
|
71
|
+
kind : str
|
|
72
|
+
``"read"`` or ``"write"``.
|
|
73
|
+
replica : int
|
|
74
|
+
The replica the client contacted.
|
|
75
|
+
value : str or None
|
|
76
|
+
The value written, or the value read; None if the request failed.
|
|
77
|
+
ok : bool
|
|
78
|
+
False if the replica refused the request to stay consistent.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
kind: str
|
|
82
|
+
replica: int
|
|
83
|
+
value: str | None
|
|
84
|
+
ok: bool
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class LookupResult:
|
|
89
|
+
"""Route of a Kademlia lookup.
|
|
90
|
+
|
|
91
|
+
Attributes
|
|
92
|
+
----------
|
|
93
|
+
path : tuple of int
|
|
94
|
+
Node identifiers visited, from the source to the node that answered.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
path: tuple[int, ...]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def hops(self) -> int:
|
|
101
|
+
"""Number of messages forwarded, one fewer than the nodes on the path."""
|
|
102
|
+
return len(self.path) - 1
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def found(self) -> int:
|
|
106
|
+
"""The node that answered, the closest to the target that the route reached."""
|
|
107
|
+
return self.path[-1]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class RelayCost:
|
|
112
|
+
"""Traffic and time to relay one block from one source to every reachable peer.
|
|
113
|
+
|
|
114
|
+
Attributes
|
|
115
|
+
----------
|
|
116
|
+
messages : int
|
|
117
|
+
Messages of any kind sent.
|
|
118
|
+
bytes : int
|
|
119
|
+
Total bytes sent.
|
|
120
|
+
completion : int
|
|
121
|
+
Time until the last peer has the block, in one-way link latencies.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
messages: int
|
|
125
|
+
bytes: int
|
|
126
|
+
completion: int
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class CompactBlockResult:
|
|
131
|
+
"""Bytes needed to relay one block in full and as a compact block (BIP 152).
|
|
132
|
+
|
|
133
|
+
Attributes
|
|
134
|
+
----------
|
|
135
|
+
full_bytes : int
|
|
136
|
+
Header plus every transaction.
|
|
137
|
+
compact_bytes : int
|
|
138
|
+
Header, nonce, short IDs, and the transactions the receiver lacked.
|
|
139
|
+
missing : int
|
|
140
|
+
Transactions the receiver had to request: absent from its mempool, or
|
|
141
|
+
ambiguous because their short ID collided.
|
|
142
|
+
round_trips : int
|
|
143
|
+
1 if the mempool covered the block, 2 if a request was needed.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
full_bytes: int
|
|
147
|
+
compact_bytes: int
|
|
148
|
+
missing: int
|
|
149
|
+
round_trips: int
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Concrete network models: topologies, clocks, dissemination, overlays, and relay."""
|
|
2
|
+
|
|
3
|
+
from blockchainkit.network.systems.addresses import AddressManager, eclipse_probability
|
|
4
|
+
from blockchainkit.network.systems.broadcast import reliable_broadcast
|
|
5
|
+
from blockchainkit.network.systems.clocks import (
|
|
6
|
+
concurrent,
|
|
7
|
+
happened_before,
|
|
8
|
+
lamport_timestamps,
|
|
9
|
+
vector_timestamps,
|
|
10
|
+
)
|
|
11
|
+
from blockchainkit.network.systems.epidemics import pittel_rounds, spread_rumor
|
|
12
|
+
from blockchainkit.network.systems.gossip import SimulatedNetwork
|
|
13
|
+
from blockchainkit.network.systems.kademlia import KademliaNetwork, node_id, xor_distance
|
|
14
|
+
from blockchainkit.network.systems.privacy import first_spy_precision
|
|
15
|
+
from blockchainkit.network.systems.propagation import fork_rate, simulate_fork_rate
|
|
16
|
+
from blockchainkit.network.systems.relay import compact_block_relay, relay_cost, short_id
|
|
17
|
+
from blockchainkit.network.systems.replication import ReplicatedRegister
|
|
18
|
+
from blockchainkit.network.systems.topology import (
|
|
19
|
+
Graph,
|
|
20
|
+
barabasi_albert,
|
|
21
|
+
complete_graph,
|
|
22
|
+
erdos_renyi,
|
|
23
|
+
ring_lattice,
|
|
24
|
+
watts_strogatz,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AddressManager",
|
|
29
|
+
"eclipse_probability",
|
|
30
|
+
"reliable_broadcast",
|
|
31
|
+
"concurrent",
|
|
32
|
+
"happened_before",
|
|
33
|
+
"lamport_timestamps",
|
|
34
|
+
"vector_timestamps",
|
|
35
|
+
"pittel_rounds",
|
|
36
|
+
"spread_rumor",
|
|
37
|
+
"SimulatedNetwork",
|
|
38
|
+
"KademliaNetwork",
|
|
39
|
+
"node_id",
|
|
40
|
+
"xor_distance",
|
|
41
|
+
"first_spy_precision",
|
|
42
|
+
"fork_rate",
|
|
43
|
+
"simulate_fork_rate",
|
|
44
|
+
"compact_block_relay",
|
|
45
|
+
"relay_cost",
|
|
46
|
+
"short_id",
|
|
47
|
+
"ReplicatedRegister",
|
|
48
|
+
"Graph",
|
|
49
|
+
"barabasi_albert",
|
|
50
|
+
"complete_graph",
|
|
51
|
+
"erdos_renyi",
|
|
52
|
+
"ring_lattice",
|
|
53
|
+
"watts_strogatz",
|
|
54
|
+
]
|