foxmath 0.6.0__tar.gz → 0.6.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -21,6 +21,8 @@ Dynamic: license-file
21
21
 
22
22
  **Minimalist CLI for math & cryptography exploration.**
23
23
 
24
+ **More about me / other projects:** [abhrankan.netlify.app](https://abhrankan.netlify.app)
25
+
24
26
  One binary. Clean output. Zero footguns. Educational by design.
25
27
 
26
28
  Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
@@ -30,12 +32,14 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
30
32
  - Euler/Hermann Machin-like π approximation
31
33
  - Elliptic curve point addition (over finite fields), including
32
34
  named curves (secp256k1) with on-curve result validation
35
+ - Elliptic curve scalar multiplication k·P (double-and-add), including
36
+ the point at infinity as a first-class result
33
37
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
34
38
  - Continued fraction expansion and convergents
35
39
  - Challenge/quiz mode — practice problems with scoring, or a
36
40
  worksheet mode for self-study
37
41
  - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd,
38
- foxmath-crt, foxmath-cf, foxmath-challenge)
42
+ foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
39
43
  - JSON output mode
40
44
 
41
45
  ## Installation
@@ -51,6 +55,14 @@ cd foxmath
51
55
  pip install -e .
52
56
  ```
53
57
 
58
+ Check which version you have installed:
59
+ ```bash
60
+ foxmath --version
61
+ ```
62
+ ```
63
+ foxmath 0.6.2
64
+ ```
65
+
54
66
  ## Usage Examples
55
67
 
56
68
  ```bash
@@ -84,6 +96,25 @@ Result point: (89565891926547004231252920425935692360644145829622209833684329913
84
96
  ✓ on curve
85
97
  ```
86
98
 
99
+ ```bash
100
+ # Scalar multiplication — same curves, O(log k) double-and-add
101
+ foxmath ecmul --curve toy193 --k 7
102
+ ```
103
+ ```
104
+ Result point: (66, 24)
105
+ ✓ on curve
106
+ ```
107
+
108
+ ```bash
109
+ # toy193: y² = x³ + x + 5 (mod 193), #E = 193 (prime) — small enough to
110
+ # hand-verify, structured exactly like secp256k1. G = (1, 59).
111
+ # k = 0 or a multiple of the group order lands on the identity:
112
+ foxmath ecmul --curve toy193 --k 193
113
+ ```
114
+ ```
115
+ Result: point at infinity (identity)
116
+ ```
117
+
87
118
  ```bash
88
119
  # Chinese Remainder Theorem
89
120
  foxmath crt --r 2 3 2 --m 3 5 7
@@ -114,8 +145,18 @@ foxmath challenge --topic legendre --count 2 --seed 1 --reveal
114
145
  Answer: -1
115
146
  ```
116
147
 
148
+ ```bash
149
+ # Baby-size discrete log: given G and Q = k·G, recover k.
150
+ # Easy at this scale — the same problem at secp256k1's ~10^77 is why ECDSA works.
151
+ foxmath challenge --topic ecdlp --count 1 --seed 9 --reveal
152
+ ```
153
+ ```
154
+ 1. On y² = x³ + 1x + 5 (mod 193) with G = (1, 59): find k (2 ≤ k ≤ 20) such that k·G = (60, 171)
155
+ Answer: 16
156
+ ```
157
+
117
158
  Drop `--reveal` to answer interactively instead, with scoring at the end.
118
- `--topic` accepts `legendre`, `crt`, or `mixed` (default).
159
+ `--topic` accepts `legendre`, `crt`, `ecdlp`, or `mixed` (default).
119
160
 
120
161
  ```bash
121
162
  # JSON output
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Minimalist CLI for math & cryptography exploration.**
4
4
 
5
+ **More about me / other projects:** [abhrankan.netlify.app](https://abhrankan.netlify.app)
6
+
5
7
  One binary. Clean output. Zero footguns. Educational by design.
6
8
 
7
9
  Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
@@ -11,12 +13,14 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
11
13
  - Euler/Hermann Machin-like π approximation
12
14
  - Elliptic curve point addition (over finite fields), including
13
15
  named curves (secp256k1) with on-curve result validation
16
+ - Elliptic curve scalar multiplication k·P (double-and-add), including
17
+ the point at infinity as a first-class result
14
18
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
15
19
  - Continued fraction expansion and convergents
16
20
  - Challenge/quiz mode — practice problems with scoring, or a
17
21
  worksheet mode for self-study
18
22
  - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd,
19
- foxmath-crt, foxmath-cf, foxmath-challenge)
23
+ foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
20
24
  - JSON output mode
21
25
 
22
26
  ## Installation
@@ -32,6 +36,14 @@ cd foxmath
32
36
  pip install -e .
33
37
  ```
34
38
 
39
+ Check which version you have installed:
40
+ ```bash
41
+ foxmath --version
42
+ ```
43
+ ```
44
+ foxmath 0.6.2
45
+ ```
46
+
35
47
  ## Usage Examples
36
48
 
37
49
  ```bash
@@ -65,6 +77,25 @@ Result point: (89565891926547004231252920425935692360644145829622209833684329913
65
77
  ✓ on curve
66
78
  ```
67
79
 
80
+ ```bash
81
+ # Scalar multiplication — same curves, O(log k) double-and-add
82
+ foxmath ecmul --curve toy193 --k 7
83
+ ```
84
+ ```
85
+ Result point: (66, 24)
86
+ ✓ on curve
87
+ ```
88
+
89
+ ```bash
90
+ # toy193: y² = x³ + x + 5 (mod 193), #E = 193 (prime) — small enough to
91
+ # hand-verify, structured exactly like secp256k1. G = (1, 59).
92
+ # k = 0 or a multiple of the group order lands on the identity:
93
+ foxmath ecmul --curve toy193 --k 193
94
+ ```
95
+ ```
96
+ Result: point at infinity (identity)
97
+ ```
98
+
68
99
  ```bash
69
100
  # Chinese Remainder Theorem
70
101
  foxmath crt --r 2 3 2 --m 3 5 7
@@ -95,8 +126,18 @@ foxmath challenge --topic legendre --count 2 --seed 1 --reveal
95
126
  Answer: -1
96
127
  ```
97
128
 
129
+ ```bash
130
+ # Baby-size discrete log: given G and Q = k·G, recover k.
131
+ # Easy at this scale — the same problem at secp256k1's ~10^77 is why ECDSA works.
132
+ foxmath challenge --topic ecdlp --count 1 --seed 9 --reveal
133
+ ```
134
+ ```
135
+ 1. On y² = x³ + 1x + 5 (mod 193) with G = (1, 59): find k (2 ≤ k ≤ 20) such that k·G = (60, 171)
136
+ Answer: 16
137
+ ```
138
+
98
139
  Drop `--reveal` to answer interactively instead, with scoring at the end.
99
- `--topic` accepts `legendre`, `crt`, or `mixed` (default).
140
+ `--topic` accepts `legendre`, `crt`, `ecdlp`, or `mixed` (default).
100
141
 
101
142
  ```bash
102
143
  # JSON output
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxmath"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Minimalist CLI for math & cryptography exploration 🦊"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -21,6 +21,8 @@ Dynamic: license-file
21
21
 
22
22
  **Minimalist CLI for math & cryptography exploration.**
23
23
 
24
+ **More about me / other projects:** [abhrankan.netlify.app](https://abhrankan.netlify.app)
25
+
24
26
  One binary. Clean output. Zero footguns. Educational by design.
25
27
 
26
28
  Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
@@ -30,12 +32,14 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
30
32
  - Euler/Hermann Machin-like π approximation
31
33
  - Elliptic curve point addition (over finite fields), including
32
34
  named curves (secp256k1) with on-curve result validation
35
+ - Elliptic curve scalar multiplication k·P (double-and-add), including
36
+ the point at infinity as a first-class result
33
37
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
34
38
  - Continued fraction expansion and convergents
35
39
  - Challenge/quiz mode — practice problems with scoring, or a
36
40
  worksheet mode for self-study
37
41
  - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd,
38
- foxmath-crt, foxmath-cf, foxmath-challenge)
42
+ foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
39
43
  - JSON output mode
40
44
 
41
45
  ## Installation
@@ -51,6 +55,14 @@ cd foxmath
51
55
  pip install -e .
52
56
  ```
53
57
 
58
+ Check which version you have installed:
59
+ ```bash
60
+ foxmath --version
61
+ ```
62
+ ```
63
+ foxmath 0.6.2
64
+ ```
65
+
54
66
  ## Usage Examples
55
67
 
56
68
  ```bash
@@ -84,6 +96,25 @@ Result point: (89565891926547004231252920425935692360644145829622209833684329913
84
96
  ✓ on curve
85
97
  ```
86
98
 
99
+ ```bash
100
+ # Scalar multiplication — same curves, O(log k) double-and-add
101
+ foxmath ecmul --curve toy193 --k 7
102
+ ```
103
+ ```
104
+ Result point: (66, 24)
105
+ ✓ on curve
106
+ ```
107
+
108
+ ```bash
109
+ # toy193: y² = x³ + x + 5 (mod 193), #E = 193 (prime) — small enough to
110
+ # hand-verify, structured exactly like secp256k1. G = (1, 59).
111
+ # k = 0 or a multiple of the group order lands on the identity:
112
+ foxmath ecmul --curve toy193 --k 193
113
+ ```
114
+ ```
115
+ Result: point at infinity (identity)
116
+ ```
117
+
87
118
  ```bash
88
119
  # Chinese Remainder Theorem
89
120
  foxmath crt --r 2 3 2 --m 3 5 7
@@ -114,8 +145,18 @@ foxmath challenge --topic legendre --count 2 --seed 1 --reveal
114
145
  Answer: -1
115
146
  ```
116
147
 
148
+ ```bash
149
+ # Baby-size discrete log: given G and Q = k·G, recover k.
150
+ # Easy at this scale — the same problem at secp256k1's ~10^77 is why ECDSA works.
151
+ foxmath challenge --topic ecdlp --count 1 --seed 9 --reveal
152
+ ```
153
+ ```
154
+ 1. On y² = x³ + 1x + 5 (mod 193) with G = (1, 59): find k (2 ≤ k ≤ 20) such that k·G = (60, 171)
155
+ Answer: 16
156
+ ```
157
+
117
158
  Drop `--reveal` to answer interactively instead, with scoring at the end.
118
- `--topic` accepts `legendre`, `crt`, or `mixed` (default).
159
+ `--topic` accepts `legendre`, `crt`, `ecdlp`, or `mixed` (default).
119
160
 
120
161
  ```bash
121
162
  # JSON output
@@ -98,6 +98,12 @@ class TestSymlinkInvocation(unittest.TestCase):
98
98
  self.assertEqual(code, 0)
99
99
  self.assertIn("Answer:", out)
100
100
 
101
+ def test_ecmul_symlink(self):
102
+ script = self._make_symlink_script("foxmath-ecmul")
103
+ code, out, err = run("--curve", "toy193", "--k", "7", script=script)
104
+ self.assertEqual(code, 0)
105
+ self.assertIn("Result point: (66, 24)", out)
106
+
101
107
  def test_symlink_with_json(self):
102
108
  script = self._make_symlink_script("foxmath-legendre")
103
109
  code, out, err = run("2", "7", "--json", script=script)
@@ -140,6 +146,58 @@ class TestNormalUsage(unittest.TestCase):
140
146
  self.assertEqual(code, 0)
141
147
  self.assertIn("Result point: (14, 2)", out)
142
148
 
149
+ def test_ecmul_readme_example_toy193(self):
150
+ # Exact usage/output shown in the README.
151
+ code, out, err = run("ecmul", "--curve", "toy193", "--k", "7")
152
+ self.assertEqual(code, 0)
153
+ self.assertIn("Result point: (66, 24)", out)
154
+ self.assertIn("on curve", out)
155
+
156
+ def test_ecmul_readme_example_identity(self):
157
+ code, out, err = run("ecmul", "--curve", "toy193", "--k", "193")
158
+ self.assertEqual(code, 0)
159
+ self.assertIn("point at infinity", out)
160
+
161
+ def test_ecmul_secp256k1_matches_known_doubling(self):
162
+ code, out, err = run("ecmul", "--curve", "secp256k1", "--k", "2")
163
+ self.assertEqual(code, 0)
164
+ self.assertIn("89565891926547004231252920425935692360644145829622209833684329913297188986597", out)
165
+
166
+ def test_ecmul_json_output(self):
167
+ code, out, err = run("ecmul", "--curve", "toy193", "--k", "7", "--json")
168
+ self.assertEqual(code, 0)
169
+ import json
170
+ parsed = json.loads(out)
171
+ self.assertEqual(parsed["result"], [66, 24])
172
+ self.assertTrue(parsed["result_on_curve"])
173
+
174
+ def test_ecmul_json_output_infinity(self):
175
+ code, out, err = run("ecmul", "--curve", "toy193", "--k", "193", "--json")
176
+ self.assertEqual(code, 0)
177
+ import json
178
+ parsed = json.loads(out)
179
+ self.assertEqual(parsed["result"], "infinity")
180
+
181
+ def test_ecmul_manual_mode_no_curve(self):
182
+ code, out, err = run("ecmul", "--p", "97", "--x", "3", "--y", "6",
183
+ "--a", "2", "--k", "3")
184
+ self.assertEqual(code, 0)
185
+ self.assertIn("Result point:", out)
186
+
187
+ def test_ecmul_a_override(self):
188
+ code, out, err = run("ecmul", "--curve", "toy97", "--a", "2", "--k", "3")
189
+ self.assertEqual(code, 0)
190
+
191
+ def test_ecmul_missing_curve_and_incomplete_manual_args_errors(self):
192
+ code, out, err = run("ecmul", "--k", "5")
193
+ self.assertEqual(code, 1)
194
+ self.assertIn("Error", err)
195
+
196
+ def test_ecmul_negative_k_errors(self):
197
+ code, out, err = run("ecmul", "--curve", "toy193", "--k", "-5")
198
+ self.assertEqual(code, 1)
199
+ self.assertIn("Error", err)
200
+
143
201
  def test_crt(self):
144
202
  code, out, err = run("crt", "--r", "2", "3", "2", "--m", "3", "5", "7")
145
203
  self.assertEqual(code, 0)
@@ -13,7 +13,7 @@ from decimal import Decimal
13
13
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
14
14
 
15
15
  from foxmath.foxmath import (
16
- legendre_symbol, euler_hermann_pi_approx, ec_point_add, crt_solve,
16
+ legendre_symbol, euler_hermann_pi_approx, ec_point_add, ec_mul, crt_solve,
17
17
  continued_fraction, cf_convergents, CURVES, is_on_curve,
18
18
  generate_problem,
19
19
  )
@@ -154,6 +154,103 @@ class TestContinuedFractions(unittest.TestCase):
154
154
  self.assertEqual(convs[-1], (expected.numerator, expected.denominator))
155
155
 
156
156
 
157
+ class TestECMul(unittest.TestCase):
158
+ """
159
+ ec_mul (scalar multiplication / double-and-add) had no test coverage
160
+ at all despite being a documented headline feature -- these tests
161
+ were added after finding and closing that gap, not in response to
162
+ any bug (cross-validation below found none).
163
+ """
164
+
165
+ def _naive_mul(self, k, x, y, a, p):
166
+ # Independent reference: k repeated additions via the
167
+ # user-facing ec_point_add, rather than ec_mul's own
168
+ # double-and-add logic.
169
+ if k == 0:
170
+ return None
171
+ result = None
172
+ point = (x, y)
173
+ for _ in range(k):
174
+ if result is None:
175
+ result = point
176
+ else:
177
+ try:
178
+ result = ec_point_add(result[0], result[1], point[0], point[1], a, p)
179
+ except ValueError:
180
+ result = None # hit the point at infinity
181
+ return result
182
+
183
+ def test_matches_naive_repeated_addition_toy97(self):
184
+ c = CURVES["toy97"]
185
+ for k in [1, 2, 3, 5, 7, 10, 13, 20]:
186
+ fast = ec_mul(k, c["gx"], c["gy"], c["a"], c["p"])
187
+ naive = self._naive_mul(k, c["gx"], c["gy"], c["a"], c["p"])
188
+ self.assertEqual(fast, naive, f"mismatch at k={k}")
189
+
190
+ def test_matches_naive_repeated_addition_toy193(self):
191
+ c = CURVES["toy193"]
192
+ for k in [1, 2, 3, 5, 7, 10, 13, 20]:
193
+ fast = ec_mul(k, c["gx"], c["gy"], c["a"], c["p"])
194
+ naive = self._naive_mul(k, c["gx"], c["gy"], c["a"], c["p"])
195
+ self.assertEqual(fast, naive, f"mismatch at k={k}")
196
+
197
+ def test_secp256k1_matches_independent_reference_including_large_k(self):
198
+ # Reference points cross-checked independently against the
199
+ # `ecdsa` library's SECP256k1.generator * k (same verification
200
+ # approach the pre-existing doubling test above uses). Values
201
+ # are baked in rather than imported at test time so this suite
202
+ # doesn't pick up a runtime dependency on `ecdsa` -- foxmath
203
+ # itself stays pure stdlib (dependencies = [] in pyproject.toml).
204
+ c = CURVES["secp256k1"]
205
+ reference = {
206
+ 1: (55066263022277343669578718895168534326250603453777594175500187360389116729240,
207
+ 32670510020758816978083085130507043184471273380659243275938904335757337482424),
208
+ 2: (89565891926547004231252920425935692360644145829622209833684329913297188986597,
209
+ 12158399299693830322967808612713398636155367887041628176798871954788371653930),
210
+ 3: (112711660439710606056748659173929673102114977341539408544630613555209775888121,
211
+ 25583027980570883691656905877401976406448868254816295069919888960541586679410),
212
+ 7: (41948375291644419605210209193538855353224492619856392092318293986323063962044,
213
+ 48361766907851246668144012348516735800090617714386977531302791340517493990618),
214
+ 100: (107303582290733097924842193972465022053148211775194373671539518313500194639752,
215
+ 103795966108782717446806684023742168462365449272639790795591544606836007446638),
216
+ 12345: (108607064596551879580190606910245687803607295064141551927605737287325610911759,
217
+ 6661302038839728943522144359728938428925407345457796456954441906546235843221),
218
+ # A scalar well beyond anything repeated addition could feasibly verify.
219
+ 2**64 + 17: (5136902638900309784563324385263118307222550852398366895311091889317425008288,
220
+ 19220983661714808463696268177333916812952649794945216772534518833957341771155),
221
+ }
222
+ for k, expected in reference.items():
223
+ fast = ec_mul(k, c["gx"], c["gy"], c["a"], c["p"])
224
+ self.assertEqual(fast, expected, f"mismatch at k={k}")
225
+
226
+ def test_toy193_has_prime_order_full_cycle(self):
227
+ # README's documented claim: #E = 193 (prime), so every non-zero
228
+ # multiple up to 192 should land on a distinct on-curve point,
229
+ # and k=193 (the order) should land on infinity.
230
+ c = CURVES["toy193"]
231
+ seen = set()
232
+ for k in range(1, 193):
233
+ pt = ec_mul(k, c["gx"], c["gy"], c["a"], c["p"])
234
+ self.assertIsNotNone(pt, f"unexpected infinity at k={k}")
235
+ self.assertTrue(is_on_curve(pt[0], pt[1], c["a"], c["b"], c["p"]))
236
+ self.assertNotIn(pt, seen, f"duplicate point at k={k} -- order is not 193")
237
+ seen.add(pt)
238
+ self.assertEqual(len(seen), 192)
239
+
240
+ def test_k_zero_is_infinity(self):
241
+ c = CURVES["toy193"]
242
+ self.assertIsNone(ec_mul(0, c["gx"], c["gy"], c["a"], c["p"]))
243
+
244
+ def test_k_equal_to_order_is_infinity(self):
245
+ c = CURVES["toy193"]
246
+ self.assertIsNone(ec_mul(193, c["gx"], c["gy"], c["a"], c["p"]))
247
+
248
+ def test_negative_k_raises(self):
249
+ c = CURVES["toy193"]
250
+ with self.assertRaises(ValueError):
251
+ ec_mul(-5, c["gx"], c["gy"], c["a"], c["p"])
252
+
253
+
157
254
  class TestNamedCurves(unittest.TestCase):
158
255
  def test_secp256k1_generator_is_on_curve(self):
159
256
  c = CURVES["secp256k1"]
@@ -193,10 +290,13 @@ class TestChallengeGenerators(unittest.TestCase):
193
290
  self.assertEqual(prob["topic"], "crt")
194
291
  self.assertIsInstance(prob["answer"], int)
195
292
 
196
- def test_mixed_topic_produces_both_kinds(self):
293
+ def test_mixed_topic_produces_all_kinds(self):
294
+ # mixed draws from every registered generator -- legendre, crt,
295
+ # and ecdlp (this assertion was stale: it predated ecdlp being
296
+ # added to _TOPIC_GENERATORS, and only checked for {legendre, crt}).
197
297
  rng = random.Random(1)
198
298
  topics = {generate_problem("mixed", rng)["topic"] for _ in range(30)}
199
- self.assertEqual(topics, {"legendre", "crt"})
299
+ self.assertEqual(topics, {"legendre", "crt", "ecdlp"})
200
300
 
201
301
  def test_reproducible_with_same_seed(self):
202
302
  p1 = generate_problem("legendre", random.Random(99))
File without changes
File without changes
File without changes
File without changes