Metadata-Version: 2.4
Name: solidus-network
Version: 0.1.1
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: blake3>=1.0
Requires-Dist: pynacl>=1.5
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: maturin>=1.0 ; extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: Python SDK for the Solidus Network — did:solidus derivation, resolution and credential verification
Keywords: did,did-solidus,decentralized-identity,verifiable-credentials,self-sovereign-identity,bbs-signatures,selective-disclosure,ed25519,w3c,solidus
License: Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/solidusnetwork/solidus-py/blob/main/CHANGELOG.md
Project-URL: Conformance vectors, https://github.com/solidusnetwork/solidus-test-vectors
Project-URL: Documentation, https://docs.solidus.network
Project-URL: Homepage, https://solidus.network
Project-URL: Issues, https://github.com/solidusnetwork/solidus-py/issues
Project-URL: Repository, https://github.com/solidusnetwork/solidus-py
Project-URL: Sibling SDK (Go), https://github.com/solidusnetwork/solidus-go
Project-URL: Sibling SDK (TypeScript), https://www.npmjs.com/org/solidus-network

# solidus-network

Python SDK for [Solidus Network](https://solidus.network) — `did:solidus` derivation, identifier
validation, W3C verification-method encoding, strict Ed25519, and BBS+ selective disclosure.

**Status: on PyPI.** `pip install solidus-network`. Wheels ship for five platforms (macOS arm64 and
x86_64, Linux x86_64 and aarch64, Windows x64) plus an sdist, published through PyPI Trusted
Publishing over OIDC, so no API token exists anywhere. Solidus runs on testnet only.

## What it does, and what it does not

**Ships:** address and DID derivation · identifier validation (SPEC v0.2.0 §4.1) ·
`publicKeyMultibase` encoding **and decoding** · strict Ed25519 verification · BBS+ signature
verification and selective-disclosure proofs.

**Not in the first version:**

- **Transaction signing.** A write path means key handling, and in Python that means people pasting
  private keys into notebooks. The read path ships well first.
- **DID resolution over the network.** The encoding and validation this package does are the parts
  implementations get wrong. An HTTP client is not.
- **SD-JWT VC issuance.**

## Install

```bash
pip install solidus-network
```

One wheel per platform serves CPython 3.9 and newer, so installing needs no Rust toolchain.

### Installing from source

A clone builds on its own. The BBS+ code the binding needs is vendored in `src/bbs.rs`, so
`Cargo.toml` has no path into the Solidus monorepo. You need a Rust toolchain:

```bash
git clone --recurse-submodules https://github.com/solidusnetwork/solidus-py.git
cd solidus-py
pip install maturin
maturin develop
```

## Usage

Every example below is executed by the test suite — `pytest --doctest-glob='*.md' README.md`. A
README that stops being true is a test failure. That is not decoration: an outside developer found
four blocks in our TypeScript README that did not compile against the published packages, and this
is the response.

### Derive an identity from a mnemonic

```python
>>> from solidus_network import seed_from_mnemonic, identity_key
>>> seed = seed_from_mnemonic(" ".join(["abandon"] * 23 + ["art"]))
>>> key = identity_key(seed)
>>> key.did("testnet")
'did:solidus:testnet:3tBoVe6XRtirzr8SdRotGgbkuEQN'

```

The identity key is `seed64[:32]` — a raw slice, deliberately **not** routed through the HKDF
hierarchy below. That asymmetry is load-bearing and frozen by the conformance vectors.

### Derive a different key for every verifier

A wallet gives each verifier its own key, so two verifiers holding the same user cannot correlate
them.

```python
>>> from solidus_network import pairwise_key
>>> a = pairwise_key(seed, "rp-a.example.com")
>>> b = pairwise_key(seed, "rp-b.example.com")
>>> a.identifier == b.identifier
False
>>> a.did()
'did:solidus:testnet:3ThmUf3VBefVuaQSBGzC1fcP5iKS'

```

### Validate a DID before you resolve it

This is what separates `invalidDid` from `notFound` — "no such DID" implies the identifier could
have existed.

```python
>>> from solidus_network import is_valid_did
>>> is_valid_did("did:solidus:testnet:3tBoVe6XRtirzr8SdRotGgbkuEQN")
True
>>> is_valid_did("did:solidus:devnet:3tBoVe6XRtirzr8SdRotGgbkuEQN")  # did-syntax:invalid-on-purpose
False
>>> is_valid_did("did:solidus:testnet:0OIl000000000000000000")  # did-syntax:invalid-on-purpose
False


```

### Read a key out of a DID Document, and verify with it

```python
>>> from solidus_network import decode_public_key_multibase, ed25519
>>> import nacl.signing
>>> signer = nacl.signing.SigningKey(b"\x07" * 32)
>>> from solidus_network import public_key_multibase
>>> verification_method = {
...     "type": "Ed25519VerificationKey2020",
...     "publicKeyMultibase": public_key_multibase(bytes(signer.verify_key)),
... }
>>> signed = signer.sign(b"a credential")
>>> ed25519.verify_multibase(
...     verification_method["publicKeyMultibase"], b"a credential", signed.signature)
True

```

Verification is **strict**: it rejects small-order keys and non-canonical encodings. The vacuous
identity equation — an all-zeros key with an all-zeros signature, which satisfies ZIP-215 for *any*
message — does not verify here.

```python
>>> ed25519.verify(bytes(32), b"anything at all", bytes(64))
False

```

### Verify a BBS+ selective-disclosure proof

The verifier never sees the undisclosed claims and never sees the issuer's signature. It sees the
proof, the issuer's public key, and the `(index, message)` pairs being asserted.

```python
>>> import pytest
>>> _ = pytest.importorskip("solidus_network.solidus_network_native")  # skips without the wheel
>>> from solidus_network import bbs
>>> sk = bytes.fromhex(
...     "363ef9668e4e1cf86b5f2092c51f7c056d6841cec69920cc5d887f68c6cab6d1")
>>> bbs.public_key_hex(sk)[:32]
'9898c245f85011e9092e9a3d20ac204d'

```

*(The `importorskip` line keeps this file runnable as a test in a clone without the compiled
extension. Your own code needs only the `from solidus_network import bbs`.)*

Full sign → prove → verify flows, including every negative case, are in `tests/test_vectors.py`
against the published conformance suite.

## API

| module | name | what it is |
|---|---|---|
| `solidus_network` | `seed_from_mnemonic(mnemonic, passphrase="")` | BIP-39 → 64-byte seed, NFKD-normalised here |
| | `identity_key(seed64)` → `DerivedKey` | `seed64[:32]`, outside the HKDF tree |
| | `pairwise_key(seed64, verifier_id)` → `DerivedKey` | HKDF-SHA512, one unlinkable key per verifier |
| | `DerivedKey.identifier` · `.did(network)` | base58 address · full `did:solidus:…` |
| `solidus_network.did` | `identifier_for(public_key)` | `base58(BLAKE3-256(key)[:20])` |
| | `did_for(public_key, network)` | the full DID string |
| | `public_key_multibase(public_key)` | `z6Mk…`, **with** the `0xed01` multicodec header |
| | `decode_public_key_multibase(mb)` | the inverse; also accepts pre-2026-08-07 headerless keys |
| | `is_valid_identifier(s)` · `is_valid_did(s)` | SPEC v0.2.0 §4.1 syntax |
| `solidus_network.ed25519` | `verify(public_key, msg, sig)` | strict; returns `False`, never raises |
| | `verify_multibase(mb, msg, sig)` | the same, straight from a DID Document |
| `solidus_network.bbs` | `public_key_hex(secret_key)` | from key **bytes**, never from IKM |
| | `verify(sig_hex, pk_hex, header, messages)` | over the full message vector |
| | `create_proof(…)` → proof hex | holder side |
| | `verify_proof(…)` | verifier side |

## Conformance

`test-vectors/` is published so third parties can check us rather than take our word. This package
runs them itself, and prints its own scope:

```
8/13 vectors CHECKED. A green run does NOT mean 13/13. Out of scope:
  credential-bundle (3) — agent-identity message map — product layer, not the protocol surface
  did-tx-create (1) — transaction signing is out of v0.1.0 — read path first
  tx-wire-binaryv2 (1) — v2 transaction wire encoding is part of the write path, out with did-tx-create
```

Eight of thirteen, stated openly, is the honest number. **An unimplemented category fails this suite —
it is never skipped.** A runner that quietly passes what it does not understand reports full marks
while checking a fraction, which is worse than having no runner: it manufactures confidence in
exactly the artifact we ask outsiders to trust.

## Ed25519 is strict, and the library choice is why

Verification must reject small-order public keys and non-canonical encodings, which is what gives
*strongly binding signatures* — exclusive ownership, the property a verifiable credential exists to
assert. ZIP-215 verification accepts both and does not.

This package depends on **PyNaCl** (libsodium) rather than `cryptography` (OpenSSL), whose Ed25519
is permissive about small-order keys. libsodium's exact behaviour varies by version, so it is
asserted in `tests/test_ed25519_strict.py` rather than promised here — a test is the only version of
this claim that stays true.

## BBS+ is native, and here is what that costs

There is no usable native Python BBS+ implementation. Checked against PyPI on 2026-08-07: `bbs` is
an empty 0.0.1 placeholder, `ursa` is gone, and `blspy`/`py_ecc` are BLS *primitives* — building on
them would mean implementing `draft-irtf-cfrg-bbs-signatures` in Python.

So BBS+ binds the Rust crate via PyO3, which buys byte-parity with the chain and costs a
per-platform wheel. Everything else is pure Python and ships in the same wheel at no native cost.
The binding is feature-gated to `bbs` only, so no C BLS toolchain enters the build.

This is why the release ships wheels: an sdist alone would force every user to have a Rust
toolchain. The release workflow (`.github/workflows/release.yml`) builds **five** wheels rather than
thirty: `pyo3`'s `abi3-py39` makes one wheel per platform serve every CPython from 3.9
up. Measured, not assumed — a wheel built on CPython 3.14 was installed on 3.9, 3.11 and 3.13 and
reproduced the frozen BBS+ public key and DID on all three.

## Development

**Without a Rust toolchain** — runs everything except the four BBS+ vectors, and says so:

```bash
git clone --recurse-submodules https://github.com/solidusnetwork/solidus-py.git
cd solidus-py
uv venv .venv && . .venv/bin/activate
uv pip install pytest blake3 pynacl
PYTHONPATH=python pytest
```

`test-vectors/` is a submodule pointing at
[solidus-test-vectors](https://github.com/solidusnetwork/solidus-test-vectors) — the same repository
we ask third parties to run against their own implementations. It is a pointer, not a copy, so the
suite here cannot quietly diverge from the one we publish.

**With a Rust toolchain**, for the full 8/13:

```bash
uv pip install maturin
maturin develop
pytest
```

`SOLIDUS_REQUIRE_NATIVE=1` turns a missing extension from a report into an error. CI sets it, so a
wheel that shipped without its native half cannot go green.

`pytest` runs the conformance vectors, every docstring example, **and** every example in this file.

Before believing the suite, break something and watch it fail. Dropping the multicodec header from
`public_key_multibase`, or making `verify()` return `True`, each takes down the specific vectors it
should and nothing else — and two of the four failures we seeded were caught by the doctests
independently of the vector runner.

## The rest of the ecosystem

The same conformance vectors back three SDKs, and all three produce byte-identical output for the
operations they share.

| language | package | BBS+ |
|---|---|---|
| Python | this package, [`solidus-network`](https://pypi.org/project/solidus-network/) | yes |
| TypeScript | [`@solidus-network/sdk`](https://www.npmjs.com/org/solidus-network) on npm | yes |
| Go | [`github.com/solidusnetwork/solidus-go`](https://github.com/solidusnetwork/solidus-go) | no, the maintained Go BBS+ libraries implement a different scheme |

The vectors themselves are at
[`solidusnetwork/solidus-test-vectors`](https://github.com/solidusnetwork/solidus-test-vectors), so
a third implementation can check itself against the same bytes.

## Licence

Apache-2.0.

