activeledger 1.3.1__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.
Files changed (29) hide show
  1. activeledger-1.3.1/LICENSE +19 -0
  2. activeledger-1.3.1/PKG-INFO +387 -0
  3. activeledger-1.3.1/README.md +364 -0
  4. activeledger-1.3.1/pyproject.toml +53 -0
  5. activeledger-1.3.1/setup.cfg +4 -0
  6. activeledger-1.3.1/src/activeledger/__init__.py +93 -0
  7. activeledger-1.3.1/src/activeledger/bip39-english.txt +2048 -0
  8. activeledger-1.3.1/src/activeledger/canonical.py +151 -0
  9. activeledger-1.3.1/src/activeledger/connection.py +128 -0
  10. activeledger-1.3.1/src/activeledger/ec.py +367 -0
  11. activeledger-1.3.1/src/activeledger/events.py +91 -0
  12. activeledger-1.3.1/src/activeledger/keys.py +99 -0
  13. activeledger-1.3.1/src/activeledger/pq.py +221 -0
  14. activeledger-1.3.1/src/activeledger/recovery.py +172 -0
  15. activeledger-1.3.1/src/activeledger/transaction.py +152 -0
  16. activeledger-1.3.1/src/activeledger.egg-info/PKG-INFO +387 -0
  17. activeledger-1.3.1/src/activeledger.egg-info/SOURCES.txt +27 -0
  18. activeledger-1.3.1/src/activeledger.egg-info/dependency_links.txt +1 -0
  19. activeledger-1.3.1/src/activeledger.egg-info/requires.txt +9 -0
  20. activeledger-1.3.1/src/activeledger.egg-info/top_level.txt +1 -0
  21. activeledger-1.3.1/tests/test_canonical.py +91 -0
  22. activeledger-1.3.1/tests/test_connection_events.py +153 -0
  23. activeledger-1.3.1/tests/test_no_ec_installed.py +82 -0
  24. activeledger-1.3.1/tests/test_no_pq_installed.py +121 -0
  25. activeledger-1.3.1/tests/test_numbers.py +68 -0
  26. activeledger-1.3.1/tests/test_pq_conformance.py +132 -0
  27. activeledger-1.3.1/tests/test_secp256k1.py +259 -0
  28. activeledger-1.3.1/tests/test_seed.py +184 -0
  29. activeledger-1.3.1/tests/test_transaction.py +149 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,387 @@
1
+ Metadata-Version: 2.4
2
+ Name: activeledger
3
+ Version: 1.3.1
4
+ Summary: Python SDK for Activeledger, with post-quantum identity support
5
+ Author: Activeledger
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/activeledger/SDK-Python
8
+ Project-URL: Ledger, https://github.com/activeledger/activeledger
9
+ Keywords: activeledger,blockchain,ledger,post-quantum,ml-dsa,falcon
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Security :: Cryptography
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: pq
17
+ Requires-Dist: liboqs-python>=0.16.0; extra == "pq"
18
+ Provides-Extra: ec
19
+ Requires-Dist: ecdsa>=0.19; extra == "ec"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ <img src="https://www.activeledger.io/wp-content/uploads/2018/09/Asset-23.png" alt="Activeledger" width="500"/>
25
+
26
+ # Activeledger SDK for Python
27
+
28
+ Python SDK for [Activeledger](https://github.com/activeledger/activeledger), with post-quantum identity support.
29
+
30
+ **Requires Activeledger 4.7.0+** for `ml-dsa-65` and `falcon-512`. Python 3.9+.
31
+
32
+ > **Rewritten.** The import root is now `activeledger`, not `activeledgerPythonSDK`, and the API changed throughout. The previous version dated from 2019, had no tests, and declared no dependencies while importing two.
33
+
34
+ ---
35
+
36
+ ## Install
37
+
38
+ > [!WARNING]
39
+ > **`pip install activeledger` does not work** — this package is not on
40
+ > PyPI yet. Install the wheel from the GitHub release:
41
+
42
+ ```bash
43
+ pip install https://github.com/activeledger/SDK-Python/releases/download/v1.3.0/activeledger_sdk-1.3.0-py3-none-any.whl
44
+ ```
45
+
46
+ Optional extras are installed alongside it:
47
+
48
+ ```bash
49
+ pip install 'ecdsa>=0.19' # secp256k1 (the [ec] extra)
50
+ pip install 'liboqs-python>=0.16' # post-quantum (the [pq] extra)
51
+ ```
52
+
53
+ The core has no dependencies. Post-quantum is opt-in because `liboqs-python`
54
+ builds liboqs from source on first import, needing git, CMake, a C compiler
55
+ and OpenSSL headers — mandatory for a client SDK would be close to unusable.
56
+
57
+ Verified: the wheel installs into a clean venv and derives keys, with the
58
+ BIP-39 wordlist included.
59
+
60
+
61
+ ## Quick start
62
+
63
+ ```python
64
+ from activeledger import Activeledger, KeyPair, KeyType
65
+
66
+ ledger = Activeledger("http://localhost:5260")
67
+
68
+ key = KeyPair.generate(KeyType.ML_DSA_65)
69
+ identity = ledger.onboard(key)
70
+ print(identity.stream_id)
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Key types
76
+
77
+ | Key type | Wire string | Public | Private | Signature | Encoding | Extra |
78
+ |---|---|---|---|---|---|---|
79
+ | ML-DSA-65 | `ml-dsa-65` | 1952 | 4032 | 3309 | base64 | `[pq]` |
80
+ | Falcon-512 | `falcon-512` | 897 | 1281 | 649-662, variable | base64 | `[pq]` |
81
+ | secp256k1 | `secp256k1` | 33 or 65 | 32 | ~70-72, variable | `0x` hex | `[ec]` |
82
+
83
+ Use **secp256k1** unless the identity must outlive a cryptographically
84
+ relevant quantum computer: roughly **22x smaller** per transaction, and every
85
+ byte is stored on the ledger permanently and replicated to every node. It also
86
+ works with hardware wallets and HSMs, and is the only way to sign for an
87
+ identity created before post-quantum support.
88
+
89
+ ```bash
90
+ pip install activeledger[ec]
91
+ ```
92
+
93
+ It installs `python-ecdsa`, which is pure Python with no build step — unlike
94
+ `coincurve` (libsecp256k1), which needs a C toolchain **and** rejects the
95
+ high-S signatures the ledger produces freely.
96
+
97
+ ```python
98
+ from activeledger import Secp256k1KeyPair
99
+
100
+ key = Secp256k1KeyPair.generate() # compressed
101
+ full = Secp256k1KeyPair.generate(compressed=False) # uncompressed
102
+
103
+ key.public_key # "0x02a1b2..." - give this to the ledger
104
+ key.private_key # store this
105
+
106
+ restored = Secp256k1KeyPair.from_keys(key.public_key, key.private_key)
107
+ verifier = Secp256k1KeyPair.from_public_key(key.public_key)
108
+ ```
109
+
110
+ ### secp256k1 is encoded nothing like the post-quantum keys
111
+
112
+ - **Keys are `0x`-prefixed hex, not base64.** The prefix is required rather
113
+ than tolerated, because hex without it can decode as base64 into
114
+ plausible-looking bytes of the wrong length.
115
+ - **Public keys have two valid lengths**, 33 compressed and 65 uncompressed,
116
+ and the ledger accepts both. A length and a SEC1 point prefix that disagree
117
+ are rejected by name.
118
+ - **Private scalars are always 32 bytes**, left-padded.
119
+ - **Signatures are SHA-256 → ECDSA → DER**, and DER length varies.
120
+
121
+ ### low-S, in both directions
122
+
123
+ **Signing** is RFC 6979 deterministic and low-S. `python-ecdsa` does **not**
124
+ normalise on its own, and it matters: without the normalisation this SDK
125
+ applies, 4 of the 12 published vectors come out high-S and differ from the
126
+ canonical form. Low-S is not for the ledger, which accepts either, but for
127
+ `@noble/curves` — the reference for the JavaScript side — and for libsecp256k1
128
+ and Rust's `k256`, all of which reject high-S by default.
129
+
130
+ **Verification accepts high-S**, because the ledger verifies through OpenSSL
131
+ and produces high-S freely. Rejecting those would fail on roughly half of all
132
+ valid signatures, and the half that succeeded would look like an intermittent
133
+ fault. `python-ecdsa` is permissive here — measured against the vectors, it
134
+ accepts all 7 high-S cases — which is why it is used.
135
+
136
+ Because signing is deterministic, this SDK's signatures are byte-identical to
137
+ `@noble/curves` for the same key and message, asserted against published
138
+ reference bytes on every test run.
139
+
140
+ `KeyType.from_wire` parses `bitcoin` and `ethereum` as secp256k1, because the
141
+ ledger routes them to identical verification. They are never emitted.
142
+
143
+ ## Post-quantum keys
144
+
145
+ | Type | Wire string | Public | Private | Signature |
146
+ | --- | --- | --- | --- | --- |
147
+ | ML-DSA-65 | `ml-dsa-65` | 1952 B | 4032 B | 3309 B, fixed |
148
+ | Falcon-512 | `falcon-512` | 897 B | 1281 B | **649-662 B, variable** |
149
+ | secp256k1 | `secp256k1` | 65 B | 32 B | ~70-72 B DER |
150
+ | RSA | `rsa` | - | - | - |
151
+
152
+ Post-quantum keys are base64 of raw algorithm bytes.
153
+
154
+ ```python
155
+ key = KeyPair.generate(KeyType.FALCON_512)
156
+
157
+ key.public_key_b64 # give this to the ledger
158
+ key.private_key_b64 # keep this
159
+ key.key_type # KeyType.FALCON_512
160
+
161
+ signature = key.sign(b"some bytes")
162
+ key.verify(b"some bytes", signature) # True
163
+
164
+ # Reload later
165
+ same = KeyPair.from_keys(KeyType.FALCON_512, pub_b64, prv_b64)
166
+
167
+ # Verify-only, no private key
168
+ checker = KeyPair.from_public(KeyType.FALCON_512, pub_b64)
169
+ ```
170
+
171
+ **Falcon signature length varies** -- never assume it fixed. **Signing is hedged** for both schemes: two signatures over the same message differ, and both verify. That matches the reference implementation; FIPS 204 permits a deterministic variant which is deliberately not used.
172
+
173
+ `verify()` returns `False` for a malformed signature rather than raising -- a caller should not have to tell "invalid" from "wrong shape".
174
+
175
+ ---
176
+
177
+ ## Seeds and recovery phrases
178
+
179
+ ```python
180
+ from activeledger import Secp256k1KeyPair, recovery
181
+
182
+ key = Secp256k1KeyPair.from_seed(seed) # 32 bytes
183
+ key = Secp256k1KeyPair.from_phrase(phrase) # BIP-39
184
+ key = Secp256k1KeyPair.from_phrase(phrase, "passphrase")
185
+ ```
186
+
187
+ The same seed gives the same identity in every Activeledger SDK, which is what
188
+ makes a seed the portable private-key format — it is how a private key moves
189
+ between languages.
190
+
191
+ A seed of the wrong length is **refused, not padded**: a padded seed is a
192
+ different identity, not a malformed one. And for `secp256k1` the seed **is**
193
+ the private scalar, so it has to be a valid one — a seed of zero, or one at or
194
+ above the curve order, is refused rather than reduced mod *n*, because
195
+ reducing produces a perfectly functional key belonging to a different identity
196
+ and nothing downstream ever reports a problem.
197
+
198
+ The phrase is validated, wordlist **and** checksum. A mistyped phrase that is
199
+ not checked does not fail; it derives a valid key for an identity nobody owns,
200
+ and the only symptom is the ledger not recognising it.
201
+
202
+ `Secp256k1KeyPair.from_legacy_phrase()` recovers a phrase made by the older
203
+ `@activeledger/sdk-bip39` package — recovery only, never for new keys.
204
+
205
+ ### Post-quantum keys cannot be derived from a seed here
206
+
207
+ **Every other Activeledger SDK can do this; this one cannot, and it is not an
208
+ oversight.** liboqs — the post-quantum backend — exposes no derandomised
209
+ signature keygen: `OQS_SIG_keypair` takes no seed and there is no
210
+ `OQS_SIG_keypair_derand`. `_keypair_derand` exists for KEMs only, verified in
211
+ the 0.14.0 headers and against liboqs `main`, so there is nothing for the
212
+ Python binding to wrap.
213
+
214
+ `KeyPair.from_seed()` raises `NotImplementedError` with that explanation
215
+ rather than being absent, so code ported from another SDK finds out here
216
+ instead of from a signature the ledger rejects.
217
+
218
+ Post-quantum identities still work fully — `generate()`, sign, verify, and
219
+ export as key bytes. Only seed derivation is unavailable, and lifting it means
220
+ moving off liboqs.
221
+
222
+ The derivation every SDK shares, for reference:
223
+
224
+ | Type | Seed from the BIP-39 seed `S` |
225
+ | --- | --- |
226
+ | `secp256k1` | `HMAC-SHA512("Bitcoin seed", S)[0..32]` |
227
+ | `ml-dsa-65` | `HKDF-SHA512(S, salt="", info="activeledger-seed-v1:ml-dsa-65", 32)` |
228
+ | `falcon-512` | `HKDF-SHA512(S, salt="", info="activeledger-seed-v1:falcon-512", 48)` |
229
+
230
+ `recovery.derive_seed()` implements all three, so the post-quantum seeds can
231
+ be derived here and used in an SDK that can consume them.
232
+
233
+ ## Transactions
234
+
235
+ ```python
236
+ from activeledger import TransactionBuilder
237
+
238
+ tx = (
239
+ TransactionBuilder()
240
+ .namespace("mynamespace")
241
+ .contract("mycontract")
242
+ .input(identity.stream_id, identity.signer, {"message": "hello"})
243
+ .output(target_stream, {"amount": 10})
244
+ .build()
245
+ )
246
+
247
+ response = ledger.submit(tx)
248
+ if not response.committed:
249
+ raise RuntimeError(response.errors)
250
+
251
+ print(response.new_streams) # stream ids created
252
+ print(response.responses) # returnToRemote values
253
+ ```
254
+
255
+ `.entry("update")` sets `$entry` for contracts with multiple entry points.
256
+
257
+ ---
258
+
259
+ ## Reading state
260
+
261
+ There is no separate read API and no storage URL. A node's storage service listens only on the node's own host, so a client cannot reach it.
262
+
263
+ State is read **through a transaction**: name streams in `$r`, and the contract hands values back with `returnToRemote`.
264
+
265
+ ```python
266
+ tx = (
267
+ TransactionBuilder()
268
+ .namespace("mynamespace")
269
+ .contract("mycontract")
270
+ .input(identity.stream_id, identity.signer)
271
+ .readonly("target", some_stream_id) # becomes $r
272
+ .build()
273
+ )
274
+
275
+ for value in ledger.submit(tx).responses:
276
+ print(value)
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Events (SSE)
282
+
283
+ Subscription is a generator, so leaving the loop closes the connection:
284
+
285
+ ```python
286
+ for event in ledger.events.subscribe():
287
+ print(event.name, event.id, event.data)
288
+ if finished:
289
+ break # connection closes here
290
+ ```
291
+
292
+ Each event is a `LedgerEvent(name, data, id)`. `name` and `id` are `None` when the server did not send them.
293
+
294
+ The parser handles the framing rules that actually matter:
295
+
296
+ - multiple `data:` lines in one event concatenate with newlines -- treating them as separate events is the classic SSE bug
297
+ - `:` comment lines (heartbeats) are ignored, not delivered as empty events
298
+ - `event:` and `id:` never leak into the following event
299
+ - an event still pending when the stream ends is delivered
300
+
301
+ Subscribe to another path with `ledger.events.subscribe("/events/mystream")`.
302
+
303
+ There is no read timeout by default: event streams are long-lived, and a timeout would close them for being quiet.
304
+
305
+ ---
306
+
307
+ ## Signing elsewhere
308
+
309
+ Anything with `key_type`, `public_key_b64` and `sign(bytes)` satisfies the `Signer` protocol, so the core needs no crypto dependency:
310
+
311
+ ```python
312
+ from activeledger import KeyType, TransactionBuilder
313
+
314
+ class HsmSigner:
315
+ key_type = KeyType.ML_DSA_65
316
+
317
+ @property
318
+ def public_key_b64(self):
319
+ return my_hsm.public_key()
320
+
321
+ def sign(self, message: bytes) -> bytes:
322
+ return my_hsm.sign(message)
323
+
324
+ tx = TransactionBuilder().namespace("n").contract("c").input(stream, HsmSigner()).build()
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Things that will bite you
330
+
331
+ **Always send the key type.** The ledger defaults a missing `type` to `"rsa"` and then attempts RSA verification against a base64 post-quantum blob. This SDK always sends it; if you hand-build an envelope, do the same.
332
+
333
+ **A rejected transaction is HTTP 200.** Check `response.committed`, never the status code.
334
+
335
+ **Errors are unhelpful by design.** A wrong type string, a wrong-length key, or signed bytes differing by one escape all come back as **1220 "Signature Incorrect"** -- never "unknown algorithm" or "bad key length". This SDK validates key lengths and type strings up front so these fail locally with a message naming the problem.
336
+
337
+ **Signatures cover `$tx` only**, not the envelope. `tx.signed_bytes()` shows exactly what was signed, which is the fastest way to diagnose a 1220.
338
+
339
+ ---
340
+
341
+ ## Canonical JSON
342
+
343
+ Signatures cover the exact bytes of `JSON.stringify($tx)` encoded UTF-8 -- no hash prefix, no length prefix, no key sorting. Python's `json.dumps` is wrong here by default in three ways, **all of which produce correct output on an ASCII-only integer payload**:
344
+
345
+ | Default | Problem |
346
+ | --- | --- |
347
+ | `ensure_ascii=True` | escapes non-ASCII to `\uXXXX` |
348
+ | `separators=(', ', ': ')` | inserts spaces |
349
+ | whole floats | prints `1.0`; JavaScript prints `1` |
350
+
351
+ `activeledger.canonical_json` handles all three, and is checked byte-for-byte against cross-language vectors generated by the JavaScript SDK.
352
+
353
+ ```python
354
+ from activeledger import canonical_json
355
+ canonical_json({"whole": 1.0, "note": "café"})
356
+ ```
357
+
358
+ ---
359
+
360
+ ## Testing
361
+
362
+ ```bash
363
+ pip install -e '.[pq,dev]'
364
+ pytest
365
+ ```
366
+
367
+ Integration tests need a live network. From an `activeledger` checkout:
368
+
369
+ ```bash
370
+ npm run test:network:serve
371
+ ```
372
+
373
+ then, with the URLs it prints:
374
+
375
+ ```bash
376
+ AL_NODES=http://127.0.0.1:5510 AL_STORAGE=http://127.0.0.1:5509 pytest tests/integration
377
+ ```
378
+
379
+ They skip when `AL_NODES` is unset.
380
+
381
+ Correctness is established against published cross-language vectors and a real 4-node network -- onboarding both post-quantum schemes, submitting transactions, and confirming a tampered payload is rejected -- not against a reading of the reference implementation.
382
+
383
+ ---
384
+
385
+ ## Licence
386
+
387
+ MIT