blockchainkit 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. blockchainkit/__init__.py +12 -0
  2. blockchainkit/_validation.py +44 -0
  3. blockchainkit/consensus/__init__.py +66 -0
  4. blockchainkit/consensus/core/__init__.py +25 -0
  5. blockchainkit/consensus/core/base.py +97 -0
  6. blockchainkit/consensus/systems/__init__.py +46 -0
  7. blockchainkit/consensus/systems/byzantine.py +91 -0
  8. blockchainkit/consensus/systems/catch_up.py +60 -0
  9. blockchainkit/consensus/systems/difficulty.py +74 -0
  10. blockchainkit/consensus/systems/finality.py +92 -0
  11. blockchainkit/consensus/systems/fork_choice.py +44 -0
  12. blockchainkit/consensus/systems/pbft.py +78 -0
  13. blockchainkit/consensus/systems/pos.py +53 -0
  14. blockchainkit/consensus/systems/pow.py +58 -0
  15. blockchainkit/consensus/systems/pricing.py +50 -0
  16. blockchainkit/consensus/systems/randomized.py +114 -0
  17. blockchainkit/consensus/systems/selfish.py +81 -0
  18. blockchainkit/consensus/systems/sortition.py +48 -0
  19. blockchainkit/consensus/systems/stake_games.py +44 -0
  20. blockchainkit/consensus/systems/synchrony.py +49 -0
  21. blockchainkit/consensus/visualizers/__init__.py +9 -0
  22. blockchainkit/consensus/visualizers/plots.py +80 -0
  23. blockchainkit/constants.py +66 -0
  24. blockchainkit/crypto/__init__.py +157 -0
  25. blockchainkit/crypto/core/__init__.py +21 -0
  26. blockchainkit/crypto/core/base.py +109 -0
  27. blockchainkit/crypto/systems/__init__.py +137 -0
  28. blockchainkit/crypto/systems/asymmetric.py +136 -0
  29. blockchainkit/crypto/systems/commitments.py +93 -0
  30. blockchainkit/crypto/systems/curves.py +205 -0
  31. blockchainkit/crypto/systems/discrete_log.py +156 -0
  32. blockchainkit/crypto/systems/hashing.py +95 -0
  33. blockchainkit/crypto/systems/lamport.py +65 -0
  34. blockchainkit/crypto/systems/mac.py +34 -0
  35. blockchainkit/crypto/systems/merkle_damgard.py +139 -0
  36. blockchainkit/crypto/systems/multisig.py +98 -0
  37. blockchainkit/crypto/systems/one_time_pad.py +42 -0
  38. blockchainkit/crypto/systems/puzzles.py +96 -0
  39. blockchainkit/crypto/systems/sharing.py +159 -0
  40. blockchainkit/crypto/systems/signatures.py +232 -0
  41. blockchainkit/crypto/utils/__init__.py +5 -0
  42. blockchainkit/crypto/utils/primes.py +39 -0
  43. blockchainkit/crypto/visualizers/__init__.py +9 -0
  44. blockchainkit/crypto/visualizers/plots.py +88 -0
  45. blockchainkit/network/__init__.py +70 -0
  46. blockchainkit/network/core/__init__.py +21 -0
  47. blockchainkit/network/core/base.py +149 -0
  48. blockchainkit/network/systems/__init__.py +54 -0
  49. blockchainkit/network/systems/addresses.py +126 -0
  50. blockchainkit/network/systems/broadcast.py +124 -0
  51. blockchainkit/network/systems/clocks.py +149 -0
  52. blockchainkit/network/systems/epidemics.py +111 -0
  53. blockchainkit/network/systems/gossip.py +201 -0
  54. blockchainkit/network/systems/kademlia.py +147 -0
  55. blockchainkit/network/systems/privacy.py +98 -0
  56. blockchainkit/network/systems/propagation.py +60 -0
  57. blockchainkit/network/systems/relay.py +123 -0
  58. blockchainkit/network/systems/replication.py +122 -0
  59. blockchainkit/network/systems/topology.py +238 -0
  60. blockchainkit/network/visualizers/__init__.py +14 -0
  61. blockchainkit/network/visualizers/plots.py +177 -0
  62. blockchainkit/py.typed +0 -0
  63. blockchainkit/structures/__init__.py +61 -0
  64. blockchainkit/structures/core/__init__.py +21 -0
  65. blockchainkit/structures/core/base.py +100 -0
  66. blockchainkit/structures/systems/__init__.py +45 -0
  67. blockchainkit/structures/systems/block.py +144 -0
  68. blockchainkit/structures/systems/bloom.py +81 -0
  69. blockchainkit/structures/systems/chain.py +131 -0
  70. blockchainkit/structures/systems/hash_chain.py +35 -0
  71. blockchainkit/structures/systems/headers.py +54 -0
  72. blockchainkit/structures/systems/ledger.py +87 -0
  73. blockchainkit/structures/systems/merkle.py +273 -0
  74. blockchainkit/structures/systems/mmr.py +112 -0
  75. blockchainkit/structures/systems/sparse_merkle.py +139 -0
  76. blockchainkit/structures/systems/transaction.py +116 -0
  77. blockchainkit/structures/systems/utxo.py +156 -0
  78. blockchainkit/structures/utils/__init__.py +6 -0
  79. blockchainkit/structures/utils/accounts.py +13 -0
  80. blockchainkit/structures/utils/encoding.py +11 -0
  81. blockchainkit/structures/visualizers/__init__.py +13 -0
  82. blockchainkit/structures/visualizers/plots.py +154 -0
  83. blockchainkit/vm/__init__.py +60 -0
  84. blockchainkit/vm/core/__init__.py +25 -0
  85. blockchainkit/vm/core/base.py +171 -0
  86. blockchainkit/vm/systems/__init__.py +40 -0
  87. blockchainkit/vm/systems/assembler.py +80 -0
  88. blockchainkit/vm/systems/expressions.py +91 -0
  89. blockchainkit/vm/systems/programs.py +143 -0
  90. blockchainkit/vm/systems/reentrancy.py +70 -0
  91. blockchainkit/vm/systems/script.py +268 -0
  92. blockchainkit/vm/systems/stack_machine.py +239 -0
  93. blockchainkit/vm/systems/turing.py +111 -0
  94. blockchainkit/vm/systems/verifier.py +81 -0
  95. blockchainkit/vm/visualizers/__init__.py +9 -0
  96. blockchainkit/vm/visualizers/plots.py +97 -0
  97. blockchainkit-0.2.0.dist-info/METADATA +195 -0
  98. blockchainkit-0.2.0.dist-info/RECORD +101 -0
  99. blockchainkit-0.2.0.dist-info/WHEEL +5 -0
  100. blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
  101. blockchainkit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,93 @@
1
+ """Commitments: salted hash commitments (Blum 1981) and Pedersen commitments (1991).
2
+
3
+ A commitment is a sealed envelope: binding (the committer cannot change the
4
+ contents later) and hiding (nobody can read them before the opening). A hash
5
+ commitment is computationally hiding and binding. A Pedersen commitment is
6
+ *perfectly* hiding and computationally binding, and commitments add.
7
+ """
8
+
9
+ import hmac
10
+
11
+ from blockchainkit._validation import integer
12
+ from blockchainkit.constants import COMMIT_DOMAIN, PEDERSEN_DOMAIN
13
+ from blockchainkit.crypto.systems.asymmetric import TEACHING_GROUP, DHGroup
14
+ from blockchainkit.crypto.systems.hashing import sha256
15
+
16
+
17
+ def commit(message: bytes, salt: bytes) -> bytes:
18
+ """Commit to a message with a secret salt of at least 16 bytes.
19
+
20
+ Parameters
21
+ ----------
22
+ message : bytes
23
+ Bytes to commit to.
24
+ salt : bytes
25
+ Random secret salt; fixed salts are only appropriate for experiments.
26
+
27
+ Returns
28
+ -------
29
+ bytes
30
+ Domain-separated digest. Length framing prevents ambiguous openings.
31
+
32
+ Notes
33
+ -----
34
+ Hiding depends on salt entropy; binding relies on collision resistance.
35
+ """
36
+ if not isinstance(message, bytes) or not isinstance(salt, bytes):
37
+ raise TypeError("message and salt must be bytes")
38
+ if len(salt) < 16:
39
+ raise ValueError("use at least 16 salt bytes")
40
+ return sha256(COMMIT_DOMAIN + len(salt).to_bytes(8, "big") + salt + message)
41
+
42
+
43
+ def verify_commitment(digest: bytes, message: bytes, salt: bytes) -> bool:
44
+ """Check an opening against a commitment using constant-time comparison."""
45
+ return hmac.compare_digest(digest, commit(message, salt))
46
+
47
+
48
+ def pedersen_generators(group: DHGroup = TEACHING_GROUP) -> tuple[int, int]:
49
+ """Return ``(g, h)``: the group generator and a second, independent generator.
50
+
51
+ ``h`` is derived by hashing the group parameters into the subgroup, so
52
+ nobody knows ``log_g(h)``. Anyone who did could open a commitment to any
53
+ value, which is why ``h`` must not be chosen by the committer.
54
+ """
55
+ counter = 0
56
+ while True:
57
+ seed = PEDERSEN_DOMAIN + f"{group.p}:{group.q}:{group.g}:{counter}".encode()
58
+ candidate = int.from_bytes(sha256(seed), "big") % group.p
59
+ h = pow(candidate, (group.p - 1) // group.q, group.p)
60
+ if h not in (0, 1, group.g):
61
+ return group.g, h
62
+ counter += 1 # Degenerate candidate (probability about 2/q): hash again.
63
+
64
+
65
+ def pedersen_commit(value: int, blinding: int, group: DHGroup = TEACHING_GROUP) -> int:
66
+ """Commit to ``value`` as ``g**value * h**blinding mod p``.
67
+
68
+ With a uniformly random blinding factor the commitment is uniformly
69
+ distributed whatever the value (perfect hiding). Opening it to a
70
+ different value would reveal ``log_g(h)`` (computational binding).
71
+ Commitments multiply to a commitment of the sum:
72
+ ``C(a, r) * C(b, s) = C(a + b, r + s)``.
73
+
74
+ Parameters
75
+ ----------
76
+ value, blinding : int
77
+ Elements of the integers modulo the group order q.
78
+ group : DHGroup
79
+ A prime-order subgroup; defaults to
80
+ :data:`~blockchainkit.crypto.systems.asymmetric.TEACHING_GROUP`.
81
+
82
+ Examples
83
+ --------
84
+ >>> from blockchainkit.crypto import pedersen_commit, TEACHING_GROUP as G
85
+ >>> pedersen_commit(2, 5) * pedersen_commit(3, 7) % G.p == pedersen_commit(5, 12)
86
+ True
87
+ """
88
+ for number, name in ((value, "value"), (blinding, "blinding")):
89
+ integer(number, name)
90
+ if number >= group.q:
91
+ raise ValueError(f"{name} must be below the group order q")
92
+ g, h = pedersen_generators(group)
93
+ return pow(g, value, group.p) * pow(h, blinding, group.p) % group.p
@@ -0,0 +1,205 @@
1
+ """Readable elliptic-curve group arithmetic, not constant-time cryptography.
2
+
3
+ Points are ``(x, y)`` tuples; ``None`` denotes the point at infinity.
4
+ The curve equation is y**2 = x**3 + a*x + b modulo p.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from math import isqrt
9
+
10
+ from blockchainkit._validation import integer
11
+ from blockchainkit.crypto.core.base import Point
12
+ from blockchainkit.crypto.utils.primes import is_prime
13
+
14
+ _SECP_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
15
+ _SECP_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Curve:
20
+ """A nonsingular prime-field curve with a prime-order generator.
21
+
22
+ Parameters
23
+ ----------
24
+ p, a, b : int
25
+ Field modulus and reduced curve coefficients.
26
+ generator : tuple of int
27
+ Affine coordinates of the base point.
28
+ order : int
29
+ Prime order of the base point.
30
+ name : str
31
+ Human-readable label.
32
+
33
+ Notes
34
+ -----
35
+ Custom field/order primes must be below 2**64, which bounds the
36
+ educational primality check. secp256k1's own field prime and group
37
+ order are also accepted, each only in its own role.
38
+ """
39
+
40
+ p: int
41
+ a: int
42
+ b: int
43
+ generator: tuple[int, int]
44
+ order: int
45
+ name: str = "custom"
46
+
47
+ def __post_init__(self) -> None:
48
+ for value, known in ((self.p, _SECP_P), (self.order, _SECP_N)):
49
+ integer(value, "prime", 3)
50
+ if value == known:
51
+ continue
52
+ if value >= 2**64:
53
+ raise ValueError("custom primes must be below 2**64")
54
+ if not is_prime(value):
55
+ raise ValueError("field modulus and generator order must be prime")
56
+ for value in (self.a, self.b):
57
+ integer(value, "coefficient")
58
+ if value >= self.p:
59
+ raise ValueError("coefficients must be reduced modulo p")
60
+ if (4 * self.a**3 + 27 * self.b**2) % self.p == 0:
61
+ raise ValueError("singular curve")
62
+ if self.generator is None or not self.contains(self.generator):
63
+ raise ValueError("generator must be a finite point on the curve")
64
+ if multiply(self.order, self.generator, self) is not None:
65
+ raise ValueError("generator does not have the claimed order")
66
+
67
+ def contains(self, point: Point) -> bool:
68
+ """Return whether point is infinity or a canonical affine curve point."""
69
+ if point is None:
70
+ return True
71
+ if not isinstance(point, tuple) or len(point) != 2:
72
+ return False
73
+ x, y = point
74
+ return (
75
+ type(x) is int
76
+ and type(y) is int
77
+ and 0 <= x < self.p
78
+ and 0 <= y < self.p
79
+ and (y * y - x**3 - self.a * x - self.b) % self.p == 0
80
+ )
81
+
82
+ @property
83
+ def cofactor_is_one(self) -> bool:
84
+ """Whether the generator's subgroup is provably the whole curve group.
85
+
86
+ Hasse's theorem bounds the number of points: #E <= p + 1 + 2*sqrt(p).
87
+ The subgroup order n divides #E, so if 2n exceeds that bound the
88
+ cofactor #E/n must be 1. Every on-curve point then lies in the
89
+ subgroup, and verifiers can skip the n*P = O membership check.
90
+ """
91
+ return 2 * self.order > self.p + 1 + 2 * (isqrt(self.p) + 1)
92
+
93
+
94
+ def _add(left: Point, right: Point, curve: Curve) -> Point:
95
+ if left is None:
96
+ return right
97
+ if right is None:
98
+ return left
99
+ x1, y1 = left
100
+ x2, y2 = right
101
+ p = curve.p
102
+ if x1 == x2 and (y1 + y2) % p == 0:
103
+ return None
104
+ if left == right:
105
+ slope = (3 * x1 * x1 + curve.a) * pow(2 * y1, -1, p) % p
106
+ else:
107
+ slope = (y2 - y1) * pow(x2 - x1, -1, p) % p
108
+ x3 = (slope * slope - x1 - x2) % p
109
+ return x3, (slope * (x1 - x3) - y1) % p
110
+
111
+
112
+ def add(left: Point, right: Point, curve: Curve) -> Point:
113
+ """Add two validated points, including infinity and inverse pairs."""
114
+ if not curve.contains(left) or not curve.contains(right):
115
+ raise ValueError("points must lie on the curve")
116
+ return _add(left, right, curve)
117
+
118
+
119
+ def multiply(scalar: int, point: Point, curve: Curve) -> Point:
120
+ """Multiply a point by a signed integer using double-and-add.
121
+
122
+ Scalars are not reduced modulo the generator order: this also allows
123
+ checking subgroup membership for arbitrary points on a custom curve.
124
+ """
125
+ if type(scalar) is not int:
126
+ raise TypeError(f"scalar must be an integer, not {type(scalar).__name__}")
127
+ if not curve.contains(point):
128
+ raise ValueError("point must lie on the curve")
129
+ if scalar < 0:
130
+ point = None if point is None else (point[0], -point[1] % curve.p)
131
+ scalar = -scalar
132
+ result = None
133
+ while scalar:
134
+ if scalar & 1:
135
+ result = _add(result, point, curve)
136
+ point = _add(point, point, curve)
137
+ scalar >>= 1
138
+ return result
139
+
140
+
141
+ TOY_CURVE = Curve(17, 2, 2, (5, 1), 19, "toy17")
142
+ SECP256K1 = Curve(
143
+ _SECP_P,
144
+ 0,
145
+ 7,
146
+ (
147
+ 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
148
+ 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
149
+ ),
150
+ _SECP_N,
151
+ "secp256k1",
152
+ )
153
+
154
+
155
+ def public_key(private: int, curve: Curve = SECP256K1) -> tuple[int, int]:
156
+ """Derive private*G for 1 <= private < generator order.
157
+
158
+ >>> from blockchainkit.crypto import public_key, TOY_CURVE
159
+ >>> public_key(2, TOY_CURVE)
160
+ (6, 3)
161
+ """
162
+ integer(private, "private", 1)
163
+ if private >= curve.order:
164
+ raise ValueError("private key must be below generator order")
165
+ point = multiply(private, curve.generator, curve)
166
+ assert point is not None
167
+ return point
168
+
169
+
170
+ def enumerate_points(curve: Curve) -> tuple[tuple[int, int], ...]:
171
+ """List every finite point of a small curve, sorted, by trying each x.
172
+
173
+ Adding the point at infinity gives the whole group, so
174
+ ``len(enumerate_points(curve)) + 1`` is the group order #E. Useful for
175
+ plotting a toy curve and for checking Hasse's bound by hand.
176
+
177
+ Raises
178
+ ------
179
+ ValueError
180
+ The field has more than 10,000 elements; enumeration is O(p).
181
+
182
+ Examples
183
+ --------
184
+ >>> from blockchainkit.crypto import TOY_CURVE, enumerate_points
185
+ >>> len(enumerate_points(TOY_CURVE)) + 1 == TOY_CURVE.order
186
+ True
187
+ """
188
+ if curve.p > 10_000:
189
+ raise ValueError("enumeration is only for small curves (p <= 10000)")
190
+ roots: dict[int, list[int]] = {}
191
+ for y in range(curve.p):
192
+ roots.setdefault(y * y % curve.p, []).append(y)
193
+ return tuple(
194
+ (x, y)
195
+ for x in range(curve.p)
196
+ for y in roots.get((x**3 + curve.a * x + curve.b) % curve.p, ())
197
+ )
198
+
199
+
200
+ def encode_point(point: Point, curve: Curve = SECP256K1) -> bytes:
201
+ """Return fixed-width uncompressed encoding (0x04 || x || y)."""
202
+ if point is None or not curve.contains(point):
203
+ raise ValueError("expected a finite on-curve point")
204
+ width = (curve.p.bit_length() + 7) // 8
205
+ return b"\x04" + point[0].to_bytes(width, "big") + point[1].to_bytes(width, "big")
@@ -0,0 +1,156 @@
1
+ """Generic discrete-logarithm algorithms: why group size and structure matter.
2
+
3
+ Baby-step giant-step (Shanks 1971) solves base**x = target in any cyclic
4
+ group of order n with about 2*sqrt(n) multiplications. Pohlig-Hellman (1978)
5
+ splits the problem along the prime factors of n, so its cost is governed by
6
+ the *largest prime factor*, not by n. Together they explain why
7
+ Diffie-Hellman and Schnorr work in a subgroup of large prime order.
8
+ """
9
+
10
+ from math import isqrt
11
+
12
+ from blockchainkit._validation import integer
13
+ from blockchainkit.crypto.core.base import DiscreteLogResult
14
+ from blockchainkit.crypto.utils.primes import is_prime
15
+
16
+ _TRIAL_DIVISION_BOUND = 2**20
17
+
18
+
19
+ def _check(base: int, target: int, modulus: int, order: int) -> None:
20
+ for value, name in ((base, "base"), (target, "target")):
21
+ integer(value, name, 1)
22
+ integer(modulus, "modulus", 3)
23
+ integer(order, "order", 1)
24
+ if base >= modulus or target >= modulus:
25
+ raise ValueError("base and target must be reduced modulo the modulus")
26
+ if pow(base, order, modulus) != 1:
27
+ raise ValueError("base**order must be 1 modulo the modulus")
28
+
29
+
30
+ def _bsgs(base: int, target: int, modulus: int, order: int) -> tuple[int, int]:
31
+ m = isqrt(order - 1) + 1 if order > 1 else 1
32
+ baby: dict[int, int] = {}
33
+ value = 1
34
+ for j in range(m):
35
+ baby.setdefault(value, j)
36
+ value = value * base % modulus
37
+ giant_step = pow(base, -m, modulus)
38
+ gamma = target
39
+ for i in range(m):
40
+ if gamma in baby:
41
+ return (i * m + baby[gamma]) % order, m + i + 1
42
+ gamma = gamma * giant_step % modulus
43
+ raise ValueError("target is not a power of base")
44
+
45
+
46
+ def baby_step_giant_step(base: int, target: int, modulus: int, order: int) -> DiscreteLogResult:
47
+ """Solve ``base**x = target (mod modulus)`` in about ``2*sqrt(order)`` steps.
48
+
49
+ Writing ``x = i*m + j`` with ``m = ceil(sqrt(order))``, a table of baby
50
+ steps ``base**j`` is matched against giant steps ``target * base**(-i*m)``.
51
+
52
+ Parameters
53
+ ----------
54
+ base : int
55
+ A group element whose order divides ``order``.
56
+ target : int
57
+ The element whose logarithm is wanted.
58
+ modulus : int
59
+ The modulus of the multiplicative group.
60
+ order : int
61
+ A multiple of the order of ``base`` (the subgroup order).
62
+
63
+ Returns
64
+ -------
65
+ DiscreteLogResult
66
+ The exponent and the number of table multiplications.
67
+
68
+ Raises
69
+ ------
70
+ ValueError
71
+ ``target`` is not a power of ``base``.
72
+
73
+ Examples
74
+ --------
75
+ >>> from blockchainkit.crypto import baby_step_giant_step
76
+ >>> baby_step_giant_step(2, pow(2, 77, 1019), 1019, 1018).exponent
77
+ 77
78
+ """
79
+ _check(base, target, modulus, order)
80
+ exponent, steps = _bsgs(base, target, modulus, order)
81
+ return DiscreteLogResult(exponent, steps)
82
+
83
+
84
+ def factor_order(n: int) -> dict[int, int]:
85
+ """Factor a group order by trial division up to 2**20.
86
+
87
+ A cofactor left over after trial division must itself be prime, which
88
+ is all Pohlig-Hellman needs; otherwise the order is rejected.
89
+
90
+ >>> from blockchainkit.crypto.systems.discrete_log import factor_order
91
+ >>> factor_order(8100)
92
+ {2: 2, 3: 4, 5: 2}
93
+ """
94
+ integer(n, "n", 1)
95
+ factors: dict[int, int] = {}
96
+ d = 2
97
+ while d * d <= n and d <= _TRIAL_DIVISION_BOUND:
98
+ while n % d == 0:
99
+ factors[d] = factors.get(d, 0) + 1
100
+ n //= d
101
+ d += 1 if d == 2 else 2
102
+ if n > 1:
103
+ if n >= 2**64 or not is_prime(n):
104
+ raise ValueError("cannot factor the order by trial division")
105
+ factors[n] = factors.get(n, 0) + 1
106
+ return factors
107
+
108
+
109
+ def pohlig_hellman(base: int, target: int, modulus: int, order: int) -> DiscreteLogResult:
110
+ """Solve a discrete log by splitting the group order into prime powers.
111
+
112
+ For each prime power ``p**e`` dividing ``order``, the problem is mapped
113
+ into the subgroup of order ``p**e`` and solved one base-``p`` digit at a
114
+ time with baby-step giant-step in a subgroup of order ``p``. The Chinese
115
+ Remainder Theorem combines the answers. The cost is about
116
+ ``sum(e * sqrt(p))``, tiny when every prime factor is small.
117
+
118
+ Parameters and return value are as for :func:`baby_step_giant_step`.
119
+
120
+ Examples
121
+ --------
122
+ >>> from blockchainkit.crypto import pohlig_hellman
123
+ >>> pohlig_hellman(6, pow(6, 4321, 8101), 8101, 8100).exponent
124
+ 4321
125
+ """
126
+ _check(base, target, modulus, order)
127
+ # Reduce the given multiple to the exact order of base, prime by prime;
128
+ # otherwise a prime-power subproblem can have a trivial generator.
129
+ factors = factor_order(order)
130
+ for prime in factors:
131
+ while factors[prime] and pow(base, order // prime, modulus) == 1:
132
+ order //= prime
133
+ factors[prime] -= 1
134
+ residues, moduli, operations = [], [], 0
135
+ for prime, power in factors.items():
136
+ if power == 0:
137
+ continue
138
+ cofactor = order // prime**power
139
+ g = pow(base, cofactor, modulus) # Order divides prime**power.
140
+ h = pow(target, cofactor, modulus)
141
+ generator = pow(g, prime ** (power - 1), modulus) # Order divides prime.
142
+ digits = 0
143
+ for k in range(power):
144
+ # Strip the digits found so far, then project onto the order-p subgroup.
145
+ residual = h * pow(g, -digits, modulus) % modulus
146
+ projected = pow(residual, prime ** (power - 1 - k), modulus)
147
+ digit, steps = _bsgs(generator, projected, modulus, prime)
148
+ operations += steps
149
+ digits += digit * prime**k
150
+ residues.append(digits)
151
+ moduli.append(prime**power)
152
+ exponent = 0
153
+ for residue, m in zip(residues, moduli, strict=True):
154
+ rest = order // m
155
+ exponent += residue * rest * pow(rest, -1, m)
156
+ return DiscreteLogResult(exponent % order, operations)
@@ -0,0 +1,95 @@
1
+ """SHA-256 hashing, bit-level digest comparison, and birthday collisions."""
2
+
3
+ import hashlib
4
+
5
+ from blockchainkit._validation import integer
6
+ from blockchainkit.crypto.core.base import CollisionResult
7
+
8
+
9
+ def sha256(data: bytes) -> bytes:
10
+ """Return the 32-byte SHA-256 digest of bytes.
11
+
12
+ >>> from blockchainkit.crypto import sha256
13
+ >>> sha256(b"abc").hex()
14
+ 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
15
+ """
16
+ if not isinstance(data, bytes):
17
+ raise TypeError("data must be bytes; encode text explicitly")
18
+ return hashlib.sha256(data).digest()
19
+
20
+
21
+ def hash256(data: bytes) -> bytes:
22
+ """Return SHA-256(SHA-256(data)), as used in Bitcoin hashing."""
23
+ return sha256(sha256(data))
24
+
25
+
26
+ def hamming_distance(left: bytes, right: bytes) -> int:
27
+ """Count differing bits between equally long byte strings.
28
+
29
+ >>> from blockchainkit.crypto import hamming_distance
30
+ >>> hamming_distance(bytes([0]), bytes([7]))
31
+ 3
32
+ """
33
+ if len(left) != len(right):
34
+ raise ValueError("inputs must have equal length")
35
+ return sum((a ^ b).bit_count() for a, b in zip(left, right, strict=True))
36
+
37
+
38
+ def truncated_hash(data: bytes, bits: int) -> int:
39
+ """Return the top ``bits`` bits of SHA-256(data) as an integer.
40
+
41
+ Truncation makes collisions findable, to measure the birthday bound.
42
+
43
+ >>> from blockchainkit.crypto import truncated_hash
44
+ >>> truncated_hash(b"abc", 8) == sha256(b"abc")[0]
45
+ True
46
+ """
47
+ integer(bits, "bits", 1)
48
+ if bits > 256:
49
+ raise ValueError("bits cannot exceed 256")
50
+ return int.from_bytes(sha256(data), "big") >> (256 - bits)
51
+
52
+
53
+ def find_collision(bits: int, *, prefix: bytes = b"", max_trials: int = 1 << 22) -> CollisionResult:
54
+ """Find two inputs whose ``bits``-bit truncated hashes agree.
55
+
56
+ Hashes ``prefix + counter`` for counter = 0, 1, 2, ... and remembers every
57
+ truncated digest. By the birthday paradox a repeat is expected after
58
+ about ``sqrt(pi/2 * 2**bits)`` trials, far fewer than the ``2**bits``
59
+ needed to hit one *given* digest (Yuval, 1979).
60
+
61
+ Parameters
62
+ ----------
63
+ bits : int
64
+ Output size to attack, between 1 and 48.
65
+ prefix : bytes
66
+ Shared prefix of every candidate input.
67
+ max_trials : int
68
+ Search bound.
69
+
70
+ Raises
71
+ ------
72
+ TimeoutError
73
+ No collision within ``max_trials``.
74
+
75
+ Examples
76
+ --------
77
+ >>> from blockchainkit.crypto import find_collision
78
+ >>> result = find_collision(16)
79
+ >>> result.first != result.second, result.trials < 2**10
80
+ (True, True)
81
+ """
82
+ integer(bits, "bits", 1)
83
+ if bits > 48:
84
+ raise ValueError("bits must be at most 48 to keep the search bounded")
85
+ integer(max_trials, "max_trials", 1)
86
+ if not isinstance(prefix, bytes):
87
+ raise TypeError("prefix must be bytes")
88
+ seen: dict[int, bytes] = {}
89
+ for counter in range(max_trials):
90
+ candidate = prefix + counter.to_bytes(8, "big")
91
+ digest = truncated_hash(candidate, bits)
92
+ if digest in seen:
93
+ return CollisionResult(seen[digest], candidate, digest, counter + 1)
94
+ seen[digest] = candidate
95
+ raise TimeoutError(f"no {bits}-bit collision in {max_trials} trials")
@@ -0,0 +1,65 @@
1
+ """Lamport one-time signatures (1979): signatures from a hash function alone.
2
+
3
+ The private key is 256 pairs of random preimages; the public key is their
4
+ hashes. To sign, hash the message and, for each digest bit, reveal the
5
+ preimage for that bit's value. Verification hashes each revealed preimage.
6
+ Each signature reveals half the private key, so a key must sign only once.
7
+ Security rests only on the hash being one-way, so the scheme survives
8
+ quantum computers, and its descendants (XMSS, SPHINCS+) are standardized.
9
+ """
10
+
11
+ import hmac
12
+
13
+ from blockchainkit.constants import LAMPORT_DOMAIN
14
+ from blockchainkit.crypto.core.base import LamportKeyPair
15
+ from blockchainkit.crypto.systems.hashing import sha256
16
+
17
+
18
+ def _bits(message: bytes) -> list[int]:
19
+ digest = int.from_bytes(sha256(message), "big")
20
+ return [(digest >> (255 - i)) & 1 for i in range(256)]
21
+
22
+
23
+ def lamport_keypair(seed: bytes) -> LamportKeyPair:
24
+ """Derive a one-time key pair deterministically from a secret seed.
25
+
26
+ Each private preimage is SHA-256(domain || seed || index || bit). A real
27
+ seed must be at least 32 random bytes and never reused.
28
+
29
+ >>> from blockchainkit.crypto import lamport_keypair
30
+ >>> len(lamport_keypair(b"seed").public)
31
+ 256
32
+ """
33
+ if not isinstance(seed, bytes):
34
+ raise TypeError("seed must be bytes")
35
+ pairs = []
36
+ for i in range(256):
37
+ prefix = LAMPORT_DOMAIN + seed + i.to_bytes(2, "big")
38
+ pairs.append((sha256(prefix + b"\x00"), sha256(prefix + b"\x01")))
39
+ public = tuple((sha256(zero), sha256(one)) for zero, one in pairs)
40
+ return LamportKeyPair(tuple(pairs), public)
41
+
42
+
43
+ def lamport_sign(message: bytes, key: LamportKeyPair) -> tuple[bytes, ...]:
44
+ """Reveal, for each bit of SHA-256(message), the matching private preimage."""
45
+ if not isinstance(message, bytes):
46
+ raise TypeError("message must be bytes")
47
+ return tuple(pair[bit] for pair, bit in zip(key.private, _bits(message), strict=True))
48
+
49
+
50
+ def lamport_verify(
51
+ message: bytes, signature: tuple[bytes, ...], public: tuple[tuple[bytes, bytes], ...]
52
+ ) -> bool:
53
+ """Check that each revealed preimage hashes to the public value for its bit.
54
+
55
+ >>> from blockchainkit.crypto import lamport_keypair, lamport_sign, lamport_verify
56
+ >>> key = lamport_keypair(b"seed")
57
+ >>> lamport_verify(b"hello", lamport_sign(b"hello", key), key.public)
58
+ True
59
+ """
60
+ if not isinstance(message, bytes) or len(signature) != 256 or len(public) != 256:
61
+ return False
62
+ return all(
63
+ isinstance(revealed, bytes) and hmac.compare_digest(sha256(revealed), pair[bit])
64
+ for revealed, pair, bit in zip(signature, public, _bits(message), strict=True)
65
+ )
@@ -0,0 +1,34 @@
1
+ """Message authentication codes: the naive secret-prefix hash, and HMAC (1996)."""
2
+
3
+ import hashlib
4
+ import hmac
5
+
6
+ from blockchainkit.crypto.systems.hashing import sha256
7
+
8
+
9
+ def naive_mac(key: bytes, message: bytes) -> bytes:
10
+ """Return SHA-256(key || message): a tempting MAC that is *broken*.
11
+
12
+ Anyone holding a tag can forge the tag of ``message || glue || suffix``
13
+ with :func:`~blockchainkit.crypto.systems.merkle_damgard.length_extension`.
14
+ Shown for contrast with :func:`hmac_sha256`; never use it.
15
+ """
16
+ if not isinstance(key, bytes) or not isinstance(message, bytes):
17
+ raise TypeError("key and message must be bytes")
18
+ return sha256(key + message)
19
+
20
+
21
+ def hmac_sha256(key: bytes, message: bytes) -> bytes:
22
+ """Return HMAC-SHA256(key, message) (Bellare, Canetti and Krawczyk, 1996).
23
+
24
+ HMAC hashes twice with two derived keys,
25
+ ``H((k XOR opad) || H((k XOR ipad) || m))``. The outer hash hides the
26
+ inner chaining state, so length extension no longer works.
27
+
28
+ >>> from blockchainkit.crypto import hmac_sha256
29
+ >>> hmac_sha256(b"key", b"The quick brown fox jumps over the lazy dog").hex()[:16]
30
+ 'f7bc83f430538424'
31
+ """
32
+ if not isinstance(key, bytes) or not isinstance(message, bytes):
33
+ raise TypeError("key and message must be bytes")
34
+ return hmac.new(key, message, hashlib.sha256).digest()