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,139 @@
1
+ """SHA-256 written out as a Merkle-Damgard iteration, and its length-extension flaw.
2
+
3
+ Merkle and Damgard (1989) showed how to hash messages of any length with a
4
+ fixed-size compression function: pad the message to whole blocks, ending with
5
+ its length, then chain ``state = compress(state, block)`` from a fixed initial
6
+ value. The digest *is* the final state. So anyone who knows ``H(m)`` and
7
+ ``len(m)`` can keep compressing from it and compute ``H(m || pad || suffix)``
8
+ without knowing ``m``: the length-extension attack. HMAC exists to stop it.
9
+
10
+ This is a readable implementation for study. Use :func:`hashlib.sha256` (or
11
+ :func:`blockchainkit.crypto.systems.hashing.sha256`) for real hashing.
12
+ """
13
+
14
+ from blockchainkit._validation import integer
15
+
16
+ SHA256_IV = (
17
+ 0x6A09E667,
18
+ 0xBB67AE85,
19
+ 0x3C6EF372,
20
+ 0xA54FF53A,
21
+ 0x510E527F,
22
+ 0x9B05688C,
23
+ 0x1F83D9AB,
24
+ 0x5BE0CD19,
25
+ )
26
+ """tuple of int: The initial chaining value (FIPS 180-4, section 5.3.3)."""
27
+
28
+ # Round constants: the first 32 bits of the fractional parts of the cube
29
+ # roots of the first 64 primes (FIPS 180-4, section 4.2.2).
30
+ _K = (
31
+ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
32
+ 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
33
+ 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
34
+ 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
35
+ 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
36
+ 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
37
+ 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
38
+ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
39
+ ) # fmt: skip
40
+ _MASK = 0xFFFFFFFF
41
+
42
+
43
+ def _rotr(x: int, n: int) -> int:
44
+ return ((x >> n) | (x << (32 - n))) & _MASK
45
+
46
+
47
+ def sha256_compress(state: tuple[int, ...], block: bytes) -> tuple[int, ...]:
48
+ """Apply SHA-256's compression function to one 64-byte block.
49
+
50
+ Parameters
51
+ ----------
52
+ state : tuple of int
53
+ Eight 32-bit words: :data:`SHA256_IV` or the previous output.
54
+ block : bytes
55
+ Exactly 64 bytes.
56
+
57
+ Returns
58
+ -------
59
+ tuple of int
60
+ The next chaining state.
61
+ """
62
+ if not isinstance(block, bytes) or len(block) != 64:
63
+ raise ValueError("a SHA-256 block is 64 bytes")
64
+ if len(state) != 8:
65
+ raise ValueError("a SHA-256 state is eight 32-bit words")
66
+ w = [int.from_bytes(block[4 * i : 4 * i + 4], "big") for i in range(16)]
67
+ for i in range(16, 64):
68
+ s0 = _rotr(w[i - 15], 7) ^ _rotr(w[i - 15], 18) ^ (w[i - 15] >> 3)
69
+ s1 = _rotr(w[i - 2], 17) ^ _rotr(w[i - 2], 19) ^ (w[i - 2] >> 10)
70
+ w.append((w[i - 16] + s0 + w[i - 7] + s1) & _MASK)
71
+ a, b, c, d, e, f, g, h = state
72
+ for i in range(64):
73
+ t1 = h + (_rotr(e, 6) ^ _rotr(e, 11) ^ _rotr(e, 25)) + ((e & f) ^ (~e & g)) + _K[i] + w[i]
74
+ t2 = (_rotr(a, 2) ^ _rotr(a, 13) ^ _rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))
75
+ a, b, c, d, e, f, g, h = (t1 + t2) & _MASK, a, b, c, (d + t1) & _MASK, e, f, g
76
+ return tuple((x + y) & _MASK for x, y in zip(state, (a, b, c, d, e, f, g, h), strict=True))
77
+
78
+
79
+ def sha256_padding(message_length: int) -> bytes:
80
+ """Return the Merkle-Damgard strengthening for a message of that many bytes.
81
+
82
+ A 0x80 byte, zeros up to 56 bytes modulo 64, then the bit length as a
83
+ 64-bit big-endian integer. Encoding the length is what makes the
84
+ construction collision-resistant whenever the compression function is.
85
+ """
86
+ integer(message_length, "message_length")
87
+ zeros = (55 - message_length) % 64
88
+ return b"\x80" + bytes(zeros) + (8 * message_length).to_bytes(8, "big")
89
+
90
+
91
+ def _iterate(state: tuple[int, ...], data: bytes) -> tuple[int, ...]:
92
+ for start in range(0, len(data), 64):
93
+ state = sha256_compress(state, data[start : start + 64])
94
+ return state
95
+
96
+
97
+ def _digest(state: tuple[int, ...]) -> bytes:
98
+ return b"".join(word.to_bytes(4, "big") for word in state)
99
+
100
+
101
+ def merkle_damgard_sha256(data: bytes) -> bytes:
102
+ """Hash by padding, then chaining the compression function from the IV.
103
+
104
+ >>> import hashlib
105
+ >>> from blockchainkit.crypto import merkle_damgard_sha256
106
+ >>> merkle_damgard_sha256(b"abc") == hashlib.sha256(b"abc").digest()
107
+ True
108
+ """
109
+ if not isinstance(data, bytes):
110
+ raise TypeError("data must be bytes")
111
+ return _digest(_iterate(SHA256_IV, data + sha256_padding(len(data))))
112
+
113
+
114
+ def length_extension(digest: bytes, original_length: int, suffix: bytes) -> tuple[bytes, bytes]:
115
+ """Extend ``H(original)`` to ``H(original || glue || suffix)`` without knowing ``original``.
116
+
117
+ Parameters
118
+ ----------
119
+ digest : bytes
120
+ SHA-256 of the unknown original message.
121
+ original_length : int
122
+ Its length in bytes (for a secret-prefix MAC, key plus message).
123
+ suffix : bytes
124
+ Data the attacker wants to append.
125
+
126
+ Returns
127
+ -------
128
+ tuple of bytes
129
+ ``(glue, forged)``: the original message's padding, which becomes
130
+ part of the forged message, and ``SHA-256(original || glue || suffix)``.
131
+ """
132
+ if not isinstance(digest, bytes) or len(digest) != 32:
133
+ raise ValueError("a SHA-256 digest is 32 bytes")
134
+ if not isinstance(suffix, bytes):
135
+ raise TypeError("suffix must be bytes")
136
+ glue = sha256_padding(original_length)
137
+ state = tuple(int.from_bytes(digest[4 * i : 4 * i + 4], "big") for i in range(8))
138
+ total = original_length + len(glue) + len(suffix)
139
+ return glue, _digest(_iterate(state, suffix + sha256_padding(total)))
@@ -0,0 +1,98 @@
1
+ """MuSig key aggregation (Maxwell, Poelstra, Seurin and Wuille, 2018).
2
+
3
+ Schnorr signatures add: if every signer uses nonce k_i and key x_i, the sum
4
+ of their responses is a valid signature for the *sum* of their public keys.
5
+ Summing keys naively is unsafe: an attacker who announces
6
+ ``Q_rogue = xG - Q_honest`` makes the sum equal ``xG``, a key they control
7
+ alone. MuSig weights each key by ``a_i = H(L, Q_i)``, a hash of the whole
8
+ key list L, so no participant can choose their key to cancel the others.
9
+
10
+ Teaching simplification: :func:`musig_sign` plays every signer in one
11
+ process. Real MuSig exchanges nonce commitments first (or uses MuSig2's two
12
+ nonces); without that round, a malicious co-signer can bias the joint nonce.
13
+ """
14
+
15
+ from collections.abc import Sequence
16
+
17
+ from blockchainkit._validation import integer
18
+ from blockchainkit.constants import MUSIG_DOMAIN
19
+ from blockchainkit.crypto.core.base import SchnorrSignature
20
+ from blockchainkit.crypto.systems.curves import (
21
+ SECP256K1,
22
+ Curve,
23
+ add,
24
+ encode_point,
25
+ multiply,
26
+ public_key,
27
+ )
28
+ from blockchainkit.crypto.systems.hashing import sha256
29
+ from blockchainkit.crypto.systems.signatures import challenge
30
+
31
+
32
+ def musig_coefficients(
33
+ publics: Sequence[tuple[int, int]], curve: Curve = SECP256K1
34
+ ) -> tuple[int, ...]:
35
+ """Return ``a_i = H(L || Q_i) mod n`` for each key, with L the encoded key list."""
36
+ if not publics:
37
+ raise ValueError("need at least one public key")
38
+ key_list = b"".join(encode_point(q, curve) for q in publics)
39
+ return tuple(
40
+ int.from_bytes(sha256(MUSIG_DOMAIN + key_list + encode_point(q, curve)), "big")
41
+ % curve.order
42
+ for q in publics
43
+ )
44
+
45
+
46
+ def aggregate_public_keys(
47
+ publics: Sequence[tuple[int, int]], curve: Curve = SECP256K1
48
+ ) -> tuple[int, int]:
49
+ """Return the MuSig aggregate key ``sum(a_i * Q_i)``.
50
+
51
+ >>> from blockchainkit.crypto import aggregate_public_keys, public_key
52
+ >>> len(aggregate_public_keys([public_key(3), public_key(5)]))
53
+ 2
54
+ """
55
+ total = None
56
+ for a, q in zip(musig_coefficients(publics, curve), publics, strict=True):
57
+ total = add(total, multiply(a, q, curve), curve)
58
+ # A weighted sum of honest keys is the point at infinity only with
59
+ # negligible probability (a hash would have to cancel the keys exactly).
60
+ assert total is not None
61
+ return total
62
+
63
+
64
+ def musig_sign(
65
+ message: bytes,
66
+ privates: Sequence[int],
67
+ nonces: Sequence[int],
68
+ curve: Curve = SECP256K1,
69
+ ) -> SchnorrSignature:
70
+ """Produce one Schnorr signature valid for the aggregate of the signers' keys.
71
+
72
+ Each signer i contributes ``R_i = k_i G`` and ``s_i = k_i + c * a_i * x_i``
73
+ with the shared challenge ``c = H(R, Q_agg, m)``; the signature is
74
+ ``(sum R_i, sum s_i)`` and verifies with :func:`~blockchainkit.crypto.systems.signatures.verify`
75
+ against :func:`aggregate_public_keys`.
76
+
77
+ Examples
78
+ --------
79
+ >>> from blockchainkit.crypto import musig_sign, aggregate_public_keys, public_key, verify
80
+ >>> signature = musig_sign(b"m", [3, 5], [11, 13])
81
+ >>> verify(b"m", signature, aggregate_public_keys([public_key(3), public_key(5)]))
82
+ True
83
+ """
84
+ if len(privates) != len(nonces) or not privates:
85
+ raise ValueError("need one nonce per signer, and at least one signer")
86
+ for nonce in nonces:
87
+ integer(nonce, "nonce", 1)
88
+ publics = [public_key(x, curve) for x in privates]
89
+ coefficients = musig_coefficients(publics, curve)
90
+ aggregate = aggregate_public_keys(publics, curve)
91
+ commitment = None
92
+ for nonce in nonces:
93
+ commitment = add(commitment, public_key(nonce, curve), curve)
94
+ if commitment is None:
95
+ raise ValueError("the joint nonce is the point at infinity; choose other nonces")
96
+ c = challenge(message, commitment, aggregate, curve)
97
+ response = sum(k + c * a * x for k, a, x in zip(nonces, coefficients, privates, strict=True))
98
+ return SchnorrSignature(commitment, response % curve.order)
@@ -0,0 +1,42 @@
1
+ """The one-time pad (Vernam 1917; Shannon 1949): XOR with a key as long as the message."""
2
+
3
+
4
+ def xor_bytes(left: bytes, right: bytes) -> bytes:
5
+ """Return the bytewise XOR of two equally long byte strings.
6
+
7
+ >>> from blockchainkit.crypto import xor_bytes
8
+ >>> xor_bytes(b"\\x0f", b"\\xff")
9
+ b'\\xf0'
10
+ """
11
+ if not isinstance(left, bytes) or not isinstance(right, bytes):
12
+ raise TypeError("xor_bytes needs bytes")
13
+ if len(left) != len(right):
14
+ raise ValueError("inputs must have equal length")
15
+ return bytes(a ^ b for a, b in zip(left, right, strict=True))
16
+
17
+
18
+ def one_time_pad(message: bytes, key: bytes) -> bytes:
19
+ """Encrypt or decrypt with a one-time pad: ``message XOR key``.
20
+
21
+ The same call decrypts, because XOR is its own inverse. Shannon proved
22
+ the pad perfectly secret when the key is uniformly random, as long as the
23
+ message, and never reused: every plaintext of that length is then equally
24
+ likely given the ciphertext. Reusing a key leaks ``m1 XOR m2``.
25
+
26
+ Parameters
27
+ ----------
28
+ message : bytes
29
+ Plaintext (or ciphertext, to decrypt).
30
+ key : bytes
31
+ A key exactly as long as the message.
32
+
33
+ Examples
34
+ --------
35
+ >>> from blockchainkit.crypto import one_time_pad
36
+ >>> ciphertext = one_time_pad(b"hi", b"\\x01\\x02")
37
+ >>> one_time_pad(ciphertext, b"\\x01\\x02")
38
+ b'hi'
39
+ """
40
+ if isinstance(message, bytes) and isinstance(key, bytes) and len(key) != len(message):
41
+ raise ValueError("the key must be exactly as long as the message")
42
+ return xor_bytes(message, key)
@@ -0,0 +1,96 @@
1
+ """Merkle's puzzles (1974-1978): key agreement from symmetric primitives alone.
2
+
3
+ Alice publishes ``count`` puzzles, each a short secret (an identifier and a
4
+ session key) encrypted under a deliberately weak key of ``bits`` bits. Bob
5
+ solves one at random, about ``2**(bits-1)`` trials, and announces only its
6
+ identifier. An eavesdropper does not know which puzzle Bob chose and must
7
+ solve, on average, half of them. With ``count`` near ``2**bits``, honest work
8
+ is linear and the attacker's quadratic: the first public-key idea, with a
9
+ gap that is only polynomial.
10
+ """
11
+
12
+ import secrets
13
+ from collections.abc import Callable
14
+
15
+ from blockchainkit._validation import integer
16
+ from blockchainkit.constants import PUZZLE_DOMAIN, PUZZLE_MAGIC
17
+ from blockchainkit.crypto.core.base import PuzzleSolution
18
+ from blockchainkit.crypto.systems.hashing import sha256
19
+ from blockchainkit.crypto.systems.one_time_pad import xor_bytes
20
+
21
+ _MAX_BITS = 24
22
+
23
+
24
+ def _keystream(weak_key: int) -> bytes:
25
+ return sha256(PUZZLE_DOMAIN + weak_key.to_bytes(8, "big"))
26
+
27
+
28
+ def merkle_puzzles(
29
+ count: int,
30
+ bits: int,
31
+ *,
32
+ randbits: Callable[[int], int] = secrets.randbits,
33
+ ) -> tuple[tuple[bytes, ...], dict[bytes, bytes]]:
34
+ """Create Alice's puzzles and her private table of identifiers to keys.
35
+
36
+ Parameters
37
+ ----------
38
+ count : int
39
+ Number of puzzles to publish.
40
+ bits : int
41
+ Size of each weak key, between 1 and 24; solving takes up to
42
+ ``2**bits`` trials.
43
+ randbits : callable
44
+ Randomness source; inject ``random.Random(seed).getrandbits`` only
45
+ for reproducible experiments.
46
+
47
+ Returns
48
+ -------
49
+ tuple
50
+ ``(puzzles, table)``: 32-byte puzzles to publish, and the mapping
51
+ from each 8-byte identifier to its 16-byte session key.
52
+ """
53
+ integer(count, "count", 1)
54
+ integer(bits, "bits", 1)
55
+ if bits > _MAX_BITS:
56
+ raise ValueError(f"bits must be at most {_MAX_BITS} to keep solving bounded")
57
+ puzzles, table = [], {}
58
+ for _ in range(count):
59
+ puzzle_id = randbits(64).to_bytes(8, "big")
60
+ key = randbits(128).to_bytes(16, "big")
61
+ weak_key = randbits(bits)
62
+ puzzles.append(xor_bytes(PUZZLE_MAGIC + puzzle_id + key, _keystream(weak_key)))
63
+ table[puzzle_id] = key
64
+ return tuple(puzzles), table
65
+
66
+
67
+ def solve_puzzle(puzzle: bytes, bits: int) -> PuzzleSolution:
68
+ """Open a puzzle by brute force over every weak key.
69
+
70
+ A trial succeeds when the decrypted plaintext starts with the known
71
+ marker, ``PUZZLE_MAGIC``.
72
+
73
+ Raises
74
+ ------
75
+ ValueError
76
+ No weak key of that size opens the puzzle.
77
+
78
+ Examples
79
+ --------
80
+ >>> from random import Random
81
+ >>> from blockchainkit.crypto import merkle_puzzles, solve_puzzle
82
+ >>> puzzles, table = merkle_puzzles(3, 6, randbits=Random(1).getrandbits)
83
+ >>> solution = solve_puzzle(puzzles[0], bits=6)
84
+ >>> table[solution.puzzle_id] == solution.key
85
+ True
86
+ """
87
+ if not isinstance(puzzle, bytes) or len(puzzle) != 32:
88
+ raise ValueError("a puzzle is 32 bytes")
89
+ integer(bits, "bits", 1)
90
+ if bits > _MAX_BITS:
91
+ raise ValueError(f"bits must be at most {_MAX_BITS} to keep solving bounded")
92
+ for weak_key in range(2**bits):
93
+ plaintext = xor_bytes(puzzle, _keystream(weak_key))
94
+ if plaintext.startswith(PUZZLE_MAGIC):
95
+ return PuzzleSolution(plaintext[8:16], plaintext[16:], weak_key + 1)
96
+ raise ValueError("no weak key of that size opens this puzzle")
@@ -0,0 +1,159 @@
1
+ """Threshold secret sharing: Shamir (1979) and Feldman's verifiable variant (1987)."""
2
+
3
+ import secrets
4
+ from collections.abc import Callable, Sequence
5
+
6
+ from blockchainkit._validation import integer
7
+ from blockchainkit.crypto.core.base import FeldmanShares
8
+ from blockchainkit.crypto.systems.asymmetric import TEACHING_GROUP, DHGroup
9
+ from blockchainkit.crypto.utils.primes import is_prime
10
+
11
+
12
+ def split_secret(
13
+ secret: int,
14
+ threshold: int,
15
+ shares: int,
16
+ prime: int = 2089,
17
+ *,
18
+ randbelow: Callable[[int], int] = secrets.randbelow,
19
+ ) -> tuple[tuple[int, int], ...]:
20
+ """Sample a polynomial with constant term ``secret`` and return its shares.
21
+
22
+ Parameters
23
+ ----------
24
+ secret : int
25
+ Field element in [0, prime).
26
+ threshold : int
27
+ Number of shares needed, between 2 and shares.
28
+ shares : int
29
+ Number of distinct nonzero evaluation points, below prime.
30
+ prime : int
31
+ Prime field modulus below 2**64.
32
+ randbelow : callable
33
+ Uniform randomness source. Inject ``random.Random(seed).randrange``
34
+ only for reproducible teaching experiments.
35
+
36
+ Returns
37
+ -------
38
+ tuple
39
+ (x, y) pairs at x = 1, ..., shares.
40
+ """
41
+ if not is_prime(prime):
42
+ raise ValueError("need a prime modulus below 2**64")
43
+ coefficients = _sample_polynomial(secret, threshold, shares, prime, randbelow)
44
+ return _evaluate(coefficients, shares, prime)
45
+
46
+
47
+ def _sample_polynomial(
48
+ secret: int, threshold: int, shares: int, prime: int, randbelow: Callable[[int], int]
49
+ ) -> list[int]:
50
+ integer(secret, "secret")
51
+ integer(threshold, "threshold", 2)
52
+ integer(shares, "shares", threshold)
53
+ if secret >= prime or shares >= prime:
54
+ raise ValueError("need secret < prime and shares < prime")
55
+ coefficients = [secret]
56
+ for _ in range(threshold - 1):
57
+ coefficient = randbelow(prime)
58
+ integer(coefficient, "random coefficient")
59
+ if coefficient >= prime:
60
+ raise ValueError("randbelow returned a value outside the field")
61
+ coefficients.append(coefficient)
62
+ # Coefficients, including the leading one, may be zero. Sampling every
63
+ # polynomial with this constant term uniformly is what makes any
64
+ # threshold - 1 shares independent of the secret (perfect secrecy).
65
+ return coefficients
66
+
67
+
68
+ def _evaluate(coefficients: list[int], shares: int, prime: int) -> tuple[tuple[int, int], ...]:
69
+ return tuple(
70
+ (x, sum(c * pow(x, i, prime) for i, c in enumerate(coefficients)) % prime)
71
+ for x in range(1, shares + 1)
72
+ )
73
+
74
+
75
+ def feldman_split(
76
+ secret: int,
77
+ threshold: int,
78
+ shares: int,
79
+ group: DHGroup = TEACHING_GROUP,
80
+ *,
81
+ randbelow: Callable[[int], int] = secrets.randbelow,
82
+ ) -> FeldmanShares:
83
+ """Shamir-share a secret modulo the group order, and publish ``g**a_j``.
84
+
85
+ Each commitment hides one polynomial coefficient in the exponent. A share
86
+ holder checks ``g**y == prod(C_j**(x**j))``, which holds exactly when
87
+ their share lies on the committed polynomial, so a cheating dealer is
88
+ caught without anyone learning the secret. The commitment ``g**secret``
89
+ is public, so the secret is only computationally hidden.
90
+
91
+ Parameters
92
+ ----------
93
+ secret : int
94
+ Element of the integers modulo the group order q.
95
+ threshold, shares : int
96
+ As for :func:`split_secret`.
97
+ group : DHGroup
98
+ Prime-order group; shares live modulo its order q.
99
+ randbelow : callable
100
+ Uniform randomness source.
101
+
102
+ Returns
103
+ -------
104
+ FeldmanShares
105
+ The shares and the public coefficient commitments.
106
+
107
+ Examples
108
+ --------
109
+ >>> from random import Random
110
+ >>> from blockchainkit.crypto import feldman_split, feldman_verify, TEACHING_GROUP
111
+ >>> dealt = feldman_split(42, 2, 3, randbelow=Random(0).randrange)
112
+ >>> all(feldman_verify(share, dealt.commitments) for share in dealt.shares)
113
+ True
114
+ """
115
+ coefficients = _sample_polynomial(secret, threshold, shares, group.q, randbelow)
116
+ commitments = tuple(pow(group.g, a, group.p) for a in coefficients)
117
+ return FeldmanShares(_evaluate(coefficients, shares, group.q), commitments)
118
+
119
+
120
+ def feldman_verify(
121
+ share: tuple[int, int], commitments: Sequence[int], group: DHGroup = TEACHING_GROUP
122
+ ) -> bool:
123
+ """Check one share against the dealer's public commitments."""
124
+ x, y = share
125
+ if type(x) is not int or type(y) is not int or not 0 < x < group.q or not 0 <= y < group.q:
126
+ return False
127
+ expected = 1
128
+ for j, commitment in enumerate(commitments):
129
+ expected = expected * pow(commitment, pow(x, j, group.q), group.p) % group.p
130
+ return pow(group.g, y, group.p) == expected
131
+
132
+
133
+ def recover_secret(shares: Sequence[tuple[int, int]], prime: int = 2089) -> int:
134
+ """Interpolate at x=0 using Lagrange coefficients.
135
+
136
+ The caller must supply enough authentic shares. Without commitments,
137
+ this function cannot detect too few shares or a malicious participant.
138
+
139
+ >>> from blockchainkit.crypto import recover_secret
140
+ >>> recover_secret([(1, 8), (2, 11)], prime=17)
141
+ 5
142
+ """
143
+ if not shares or not is_prime(prime):
144
+ raise ValueError("need shares and a prime modulus below 2**64")
145
+ for x, y in shares:
146
+ integer(x, "x", 1)
147
+ integer(y, "y")
148
+ if x >= prime or y >= prime:
149
+ raise ValueError("share coordinates must be field elements")
150
+ if len({x for x, _ in shares}) != len(shares):
151
+ raise ValueError("share x coordinates must be distinct")
152
+ result = 0
153
+ for i, (xi, yi) in enumerate(shares):
154
+ weight = 1
155
+ for j, (xj, _) in enumerate(shares):
156
+ if i != j:
157
+ weight = weight * (-xj) * pow(xi - xj, -1, prime) % prime
158
+ result = (result + yi * weight) % prime
159
+ return result