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.
- activeledger-1.3.1/LICENSE +19 -0
- activeledger-1.3.1/PKG-INFO +387 -0
- activeledger-1.3.1/README.md +364 -0
- activeledger-1.3.1/pyproject.toml +53 -0
- activeledger-1.3.1/setup.cfg +4 -0
- activeledger-1.3.1/src/activeledger/__init__.py +93 -0
- activeledger-1.3.1/src/activeledger/bip39-english.txt +2048 -0
- activeledger-1.3.1/src/activeledger/canonical.py +151 -0
- activeledger-1.3.1/src/activeledger/connection.py +128 -0
- activeledger-1.3.1/src/activeledger/ec.py +367 -0
- activeledger-1.3.1/src/activeledger/events.py +91 -0
- activeledger-1.3.1/src/activeledger/keys.py +99 -0
- activeledger-1.3.1/src/activeledger/pq.py +221 -0
- activeledger-1.3.1/src/activeledger/recovery.py +172 -0
- activeledger-1.3.1/src/activeledger/transaction.py +152 -0
- activeledger-1.3.1/src/activeledger.egg-info/PKG-INFO +387 -0
- activeledger-1.3.1/src/activeledger.egg-info/SOURCES.txt +27 -0
- activeledger-1.3.1/src/activeledger.egg-info/dependency_links.txt +1 -0
- activeledger-1.3.1/src/activeledger.egg-info/requires.txt +9 -0
- activeledger-1.3.1/src/activeledger.egg-info/top_level.txt +1 -0
- activeledger-1.3.1/tests/test_canonical.py +91 -0
- activeledger-1.3.1/tests/test_connection_events.py +153 -0
- activeledger-1.3.1/tests/test_no_ec_installed.py +82 -0
- activeledger-1.3.1/tests/test_no_pq_installed.py +121 -0
- activeledger-1.3.1/tests/test_numbers.py +68 -0
- activeledger-1.3.1/tests/test_pq_conformance.py +132 -0
- activeledger-1.3.1/tests/test_secp256k1.py +259 -0
- activeledger-1.3.1/tests/test_seed.py +184 -0
- 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
|