Skip to content
PQ Stealth Addresses

pq-stealth — executable spec

Python is the byte-level source of truth for the key-exchange technical reference. Start with pq_stealth/commit.py: ML-KEM viewing keys, commitment-format meta-addresses, sender derivation, and viewing-key scanning (D-024/D-025). pq_stealth/__init__.py exposes the separate Construction A API; it is not the commitment-format entry point.

Construction A's ML-DSA blinding/signing modules, classical hybrid, and spend helpers are supporting implementations. See ERC evidence and spending research for their boundaries. The Python library is an executable reference, not a hardened production signer.

Setup

python -m venv venv && . venv/bin/activate
pip install -e ".[dev]"          # add .[audit] for the liboqs cross-check

Run

pytest                            # full reference suite
pytest tests/test_commit.py        # commitment format / scan behavior
pytest tests/test_profiles.py      # explicit profiles, ML-DSA-44 key commitment, authorization
python vectors/generate_mldsa44_commit_vectors.py --outdir /tmp/pq-mldsa44
cmp vectors/v0/mldsa44_commit_vectors.json /tmp/pq-mldsa44/mldsa44_commit_vectors.json
python vectors/generate_commit_vectors.py --outdir /tmp/pq-commit-vectors
cmp vectors/v0/commit_vectors.json /tmp/pq-commit-vectors/commit_vectors.json
python vectors/generate_vectors.py   # regenerate vectors/v0/vectors.json
                                     # (deterministic: byte-identical output)

Docker

Self-contained image with the liboqs cross-check backend built in (pinned to match liboqs-python):

docker build -t pq-stealth-py .
docker run --rm pq-stealth-py        # full reference test suite
 
# conformance vectors: regenerate and confirm byte-identical
docker run --rm pq-stealth-py sh -c \
  "cp vectors/v0/vectors.json /tmp/ref.json \
   && python vectors/generate_vectors.py \
   && cmp vectors/v0/vectors.json /tmp/ref.json && echo vectors OK"

Layout

ModuleContents
pq_stealth/commit.pycommitment-format key exchange and address derivation (start here)
pq_stealth/profiles.pyexplicit account profiles over 0x02; ML-DSA-44 key commitment and pk ‖ opener ‖ sig authorization (D-027)
pq_stealth/params.pyparameter sets (default ML-KEM-768 + ML-DSA-65)
pq_stealth/blinding.pythe algebraic core: t' = A·s' + e' + t
pq_stealth/encoding.pymeta-address / full-t / blinded-sk packing, keccak addresses
pq_stealth/meta.pyrecipient keygen, meta-address assembly
pq_stealth/sender.pyencaps → stealth address → announcement
pq_stealth/recipient.pyscanning with view-tag fast path
pq_stealth/signing.pyblinded FIPS 204 signing, proof of possession
pq_stealth/classical/classical-spend hybrid (secp256k1 + ML-KEM); see below

Account spendability depends on the selected deployment and verifier. The reference functions derive announcements/addresses; transaction execution is handled by the separate account integrations. A raw Construction A ML-DSA public-key hash is not an ECDSA-controlled EOA.

Classical-spend hybrid (secp256k1)

An alternative construction that keeps the ML-KEM key exchange but blinds a secp256k1 spending key, so the stealth output is a normal EOA, spendable today with a plain ECDSA transaction. It reuses this package's Announcement, view-tag, and ERC-6538 registry rail; only the blinding backend and the EOA address rule differ. Full write-up: docs/classical-spend-hybrid.md.

It is opt-in: the only dependency beyond the core is coincurve (the bench extra).

pip install -e ".[bench]"                     # adds coincurve (libsecp256k1)
pytest tests/test_classical_*.py              # roundtrip, negative, vectors (18 tests)
python vectors/generate_classical_vectors.py  # regenerate vectors/classical/v0
                                              # (deterministic: byte-identical output)

End-to-end in a few lines (send, detect, derive the EOA key, prove control):

from pq_stealth.classical import (
    gen_meta_address, send, check_announcement,
    derive_stealth_privkey, eth_address,
)
 
meta_pub, meta_priv = gen_meta_address()                                # recipient: secp256k1 + ML-KEM keys
ann = send(meta_pub)                                                    # sender: ML-KEM encaps -> stealth EOA
pay = check_announcement(meta_pub, meta_priv.kem_dk, ann)               # recipient: scan + detect
priv = derive_stealth_privkey(meta_priv.spend_priv, pay.shared_secret)
assert eth_address(priv.public_key) == ann.stealth_address              # a plain ECDSA key controls it

Unlike the ML-DSA scheme above, value sent here is spendable immediately: priv is an ordinary secp256k1 key.

ModuleContents
pq_stealth/classical/params.pyparameter sets (default secp256k1 + ML-KEM-768)
pq_stealth/classical/blinding.pythe algebraic core: t = KDF(ss) mod n, P = K + t·G
pq_stealth/classical/encoding.pymeta-address packing, EOA keccak addresses
pq_stealth/classical/meta.pyrecipient keygen, meta-address assembly
pq_stealth/classical/sender.pyencaps → stealth EOA → announcement
pq_stealth/classical/recipient.pyscanning with view-tag fast path