foxmath 0.6.1__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.1
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.
@@ -58,7 +60,7 @@ Check which version you have installed:
58
60
  foxmath --version
59
61
  ```
60
62
  ```
61
- foxmath 0.6.1
63
+ foxmath 0.6.2
62
64
  ```
63
65
 
64
66
  ## Usage Examples
@@ -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.
@@ -39,7 +41,7 @@ Check which version you have installed:
39
41
  foxmath --version
40
42
  ```
41
43
  ```
42
- foxmath 0.6.1
44
+ foxmath 0.6.2
43
45
  ```
44
46
 
45
47
  ## Usage Examples
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxmath"
7
- version = "0.6.1"
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.1
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.
@@ -58,7 +60,7 @@ Check which version you have installed:
58
60
  foxmath --version
59
61
  ```
60
62
  ```
61
- foxmath 0.6.1
63
+ foxmath 0.6.2
62
64
  ```
63
65
 
64
66
  ## Usage Examples
@@ -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