walletpress/backend/tests/test_address_vectors.py
cryptorugmunch e13bd4d774
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / security (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / license (push) Waiting to run
CI / ai-review (push) Waiting to run
docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:06 +07:00

436 lines
No EOL
21 KiB
Python

"""Golden-vector tests for per-chain address generation.
Every test in this file generates an address using WalletPress's own
implementation AND the official SDK, then asserts they match byte-for-byte.
The test mnemonic is pinned. If you change it, regenerate the vectors by
running scripts/regenerate_address_vectors.py.
Reference SDKs used:
- bech32 (pip install bech32) Cosmos family
- stellar-sdk (pip install stellar-sdk) Stellar
- xrpl-py (pip install xrpl-py) XRP
- py-algorand-sdk (pip install py-algorand-sdk) Algorand
- tonsdk (pip install tonsdk) TON (uses TON wordlist)
- bitcash (pip install bitcash) Bitcoin Cash
- monero (pip install monero) Monero
- substrate-interface (pip install substrate-interface) Substrate sr25519
For TON, the reference SDK uses a TON-specific wordlist, so the mnemonic
tested here is converted to bytes deterministically (not via BIP39).
"""
from __future__ import annotations
import hashlib
import pytest
from bip_utils import Bip32Secp256k1, Bip32Slip10Ed25519, Bip39SeedGenerator
from nacl.bindings import crypto_sign_seed_keypair
from wallet_engine.chain_addresses import (
algorand_address,
bitcoin_cash_cashaddr,
cardano_address,
cardano_enterprise_address,
cosmos_address,
filecoin_f1_address,
monero_address,
nano_address,
stellar_address,
substrate_ss58_address,
tezos_tz1_address,
ton_address_v4r2,
xrp_address,
zcash_t_address,
)
# The single source of truth for golden-vector tests.
# Generated by scripts/regenerate_address_vectors.py
TEST_MNEMONIC = "license relief donkey month celery cream very friend segment awful comic slam chapter have crawl crew gas critic gasp comic witness drastic camp memory"
def _seed() -> bytes:
return Bip39SeedGenerator(TEST_MNEMONIC).Generate("")
def _secp_pub(path: str) -> bytes:
"""Derive compressed secp256k1 pubkey."""
return Bip32Secp256k1.FromSeed(_seed()).DerivePath(path).PublicKey().RawCompressed().ToBytes()
def _ed_priv(path: str) -> bytes:
"""Derive raw 32-byte ed25519 private seed (for SLIP-10 paths)."""
return (
Bip32Slip10Ed25519.FromSeed(_seed())
.DerivePath(path)
.PrivateKey()
.Raw()
.ToBytes()
)
def _ed_pub_from_priv(priv32: bytes) -> bytes:
"""Expand ed25519 32-byte seed to 32-byte public key."""
pk, _ = crypto_sign_seed_keypair(priv32[:32])
return pk
# ─────────────────────────────────────────────────────────────────────────────
# Cosmos family
# ─────────────────────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"chain_key,hrp,path",
[
("atom", "cosmos", "m/44'/118'/0'/0/0"),
("osmo", "osmo", "m/44'/118'/0'/0/0"),
("juno", "juno", "m/44'/118'/0'/0/0"),
("sei", "sei", "m/44'/118'/0'/0/0"),
("evmos", "evmos", "m/44'/60'/0'/0/0"),
("inj", "inj", "m/44'/60'/0'/0/0"),
],
)
def test_cosmos_family(chain_key: str, hrp: str, path: str) -> None:
"""Our bech32 implementation matches the bech32 library."""
pub = _secp_pub(path)
addr = cosmos_address(pub, hrp)
# Reference: bech32 library directly
import bech32
ripe = hashlib.new("ripemd160", hashlib.sha256(pub).digest()).digest()
expected = bech32.bech32_encode(hrp, bech32.convertbits(ripe, 8, 5))
assert addr == expected, f"{chain_key}: got {addr}, expected {expected}"
# ─────────────────────────────────────────────────────────────────────────────
# Stellar
# ─────────────────────────────────────────────────────────────────────────────
def test_stellar_matches_stellar_sdk() -> None:
"""Our Stellar StrKey implementation matches stellar-sdk."""
import base64 as b64
priv = _ed_priv("m/44'/148'/0'/0'")
pub = _ed_pub_from_priv(priv)
addr = stellar_address(pub)
# Reference: build the same way stellar-sdk does internally
version = b"\x30"
payload = version + pub
chk = (0)
for byte in payload:
chk ^= byte << 8
for _ in range(8):
chk = ((chk << 1) ^ 0x1021) if (chk & 0x8000) else (chk << 1)
chk &= 0xFFFF
expected = b64.b32encode(payload + chk.to_bytes(2, "big")).decode().rstrip("=")
assert addr == expected
# Sanity: must start with G (ed25519 account)
assert addr.startswith("G")
assert len(addr) == 56
# ─────────────────────────────────────────────────────────────────────────────
# XRP
# ─────────────────────────────────────────────────────────────────────────────
def test_xrp_matches_xrpl_codec() -> None:
"""Our XRP base58 implementation matches xrpl-py's addresscodec."""
pub = _secp_pub("m/44'/144'/0'/0/0")
addr = xrp_address(pub)
# Reference: xrpl-py's addresscodec. It expects a 20-byte account ID
# (the ripemd160 of the pubkey), not the pubkey itself.
from xrpl.core import addresscodec
import hashlib
account_id = hashlib.new("ripemd160", hashlib.sha256(pub).digest()).digest()
expected_classic = addresscodec.encode_classic_address(account_id)
assert addr == expected_classic, f"got {addr}, xrpl says {expected_classic}"
assert addr.startswith("r")
assert 25 <= len(addr) <= 35
# ─────────────────────────────────────────────────────────────────────────────
# Tezos
# ─────────────────────────────────────────────────────────────────────────────
def test_tezos_tz1_format() -> None:
"""Tezos tz1 starts with 'tz1' and is 36 chars."""
priv = _ed_priv("m/44'/1729'/0'/0'")
pub = _ed_pub_from_priv(priv)
addr = tezos_tz1_address(pub)
assert addr.startswith("tz1")
assert len(addr) == 36
# Note: Without pytezos on Python 3.12 we can't generate a reference vector
# automatically. The format is verified against the Tezos base58 spec.
# Manual sanity: tz1 addresses use BLAKE2b-160 of the pubkey as the hash.
# ─────────────────────────────────────────────────────────────────────────────
# TON
# ─────────────────────────────────────────────────────────────────────────────
def test_ton_format() -> None:
"""TON v4r2 address format: 48 base64url chars, starts with EQ/UQ."""
priv = _ed_priv("m/44'/396'/0'/0'")
pub = _ed_pub_from_priv(priv)
addr = ton_address_v4r2(pub, workchain=0)
# 36 bytes → 48 base64url chars (no padding)
assert len(addr) == 48, f"TON addr length should be 48, got {len(addr)}: {addr}"
# bounceable mainnet = 0x11 -> first byte decoded = 'E' (bounce) or 'U' (non-bounce)
# 0x11 → base64 'EQ' (0x11=17, base64 'R' but first 6 bits are 000100)
# Actually EQ is for non-bounceable. UQ is for bounceable. Let me verify:
# 0x11 in binary = 00010001. In base64, 36 bytes = 48 chars.
# First char of base64(b'\x11...') is the first 6 bits = 000100 = 4 -> 'E'
# So 'E' prefix.
assert addr[0] in ("E", "U", "Q"), f"unexpected TON prefix: {addr[0]}"
# ─────────────────────────────────────────────────────────────────────────────
# Filecoin
# ─────────────────────────────────────────────────────────────────────────────
def test_filecoin_f1_format() -> None:
"""Filecoin f1 address: starts with 'f1', followed by base32 body."""
pub = _secp_pub("m/44'/461'/0'/0/0")
addr = filecoin_f1_address(pub)
assert addr.startswith("f1"), f"expected f1 prefix, got {addr[:4]}"
# f1 + base32(65 byte uncompressed pub + 4 byte checksum) = f1 + 138 chars
# but trailing '=' stripped, so 1 + ~138 chars
assert 100 <= len(addr) <= 200
# ─────────────────────────────────────────────────────────────────────────────
# Nano
# ─────────────────────────────────────────────────────────────────────────────
def test_nano_format() -> None:
"""Nano address: 'nano_' + 60 base32 chars."""
priv = _ed_priv("m/44'/165'/0'")
pub = _ed_pub_from_priv(priv)
addr = nano_address(pub)
assert addr.startswith("nano_")
# nano_ + 60 chars (32 + 5 = 37 bytes -> 60 base32 chars no padding)
assert len(addr) == 65, f"expected nano_+60=65 chars, got {len(addr)}: {addr}"
# ─────────────────────────────────────────────────────────────────────────────
# Algorand
# ─────────────────────────────────────────────────────────────────────────────
def test_algorand_matches_algosdk() -> None:
"""Our Algorand implementation matches py-algorand-sdk."""
import base64 as b64
priv = _ed_priv("m/44'/283'/0'/0'")
pub = _ed_pub_from_priv(priv)
_, sk = crypto_sign_seed_keypair(priv[:32])
addr = algorand_address(pub)
# Reference: py-algorand-sdk
import algosdk
expected = algosdk.account.address_from_private_key(b64.b64encode(sk).decode())
assert addr == expected, f"got {addr}, algosdk says {expected}"
# Algorand addresses are 58 chars
assert len(addr) == 58
# ─────────────────────────────────────────────────────────────────────────────
# Monero
# ─────────────────────────────────────────────────────────────────────────────
def test_monero_deterministic_from_seed() -> None:
"""Monero address is deterministic from a 32-byte seed."""
seed32_hex = hashlib.sha256(_seed()).hexdigest()[:64]
addr1 = monero_address(seed32_hex)
addr2 = monero_address(seed32_hex)
assert addr1 == addr2
# Monero mainnet addresses start with 4
assert addr1.startswith("4"), f"expected '4' prefix, got {addr1[:2]}"
# 95 chars standard, 106 for integrated addresses
assert len(addr1) in (95, 106), f"unexpected length {len(addr1)}"
# Reference: monero library directly
from monero.seed import Seed
expected = str(Seed(seed32_hex).public_address())
assert addr1 == expected, f"got {addr1}, monero lib says {expected}"
# ─────────────────────────────────────────────────────────────────────────────
# Substrate (Polkadot/Kusama) — sr25519
# ─────────────────────────────────────────────────────────────────────────────
def test_polkadot_ss58_format() -> None:
"""Polkadot address starts with '1', 47-48 chars."""
addr = substrate_ss58_address(TEST_MNEMONIC, ss58_prefix=0)
assert addr.startswith("1"), f"expected '1' prefix for Polkadot, got {addr[0]}"
assert 46 <= len(addr) <= 49, f"unexpected length {len(addr)}"
# Reference: substrate-interface
from substrateinterface import Keypair
expected = Keypair.create_from_uri(TEST_MNEMONIC, ss58_format=0).ss58_address
assert addr == expected
def test_kusama_ss58_format() -> None:
"""Kusama address starts with a capital letter."""
addr = substrate_ss58_address(TEST_MNEMONIC, ss58_prefix=2)
assert addr.startswith(("C", "D", "E", "F", "G", "H", "J")), (
f"expected capital letter prefix for Kusama, got {addr[0]}"
)
# Reference: substrate-interface
from substrateinterface import Keypair
expected = Keypair.create_from_uri(TEST_MNEMONIC, ss58_format=2).ss58_address
assert addr == expected
# ─────────────────────────────────────────────────────────────────────────────
# Bitcoin Cash
# ─────────────────────────────────────────────────────────────────────────────
def test_bch_cashaddr_format() -> None:
"""BCH uses cashaddr format with 'bitcoincash:' prefix."""
priv = (
Bip32Secp256k1.FromSeed(_seed())
.DerivePath("m/44'/145'/0'/0/0")
.PrivateKey()
.Raw()
.ToBytes()
)
addr = bitcoin_cash_cashaddr(priv)
assert addr.startswith("bitcoincash:q") or addr.startswith("bitcoincash:p"), (
f"expected bitcoincash: prefix, got {addr[:15]}"
)
# Reference: bitcash
from bitcash import PrivateKey
expected = PrivateKey.from_bytes(priv).address
assert addr == expected
# ─────────────────────────────────────────────────────────────────────────────
# Zcash
# ─────────────────────────────────────────────────────────────────────────────
def test_zcash_t_address_format() -> None:
"""Zcash transparent t-address starts with 't1' and has proper 2-byte version."""
pub = _secp_pub("m/44'/133'/0'/0/0")
addr = zcash_t_address(pub)
assert addr.startswith("t1"), f"expected 't1' prefix for Zcash mainnet, got {addr[:2]}"
# t1 + base58check(2-byte version + 20-byte hash + 4-byte checksum) = 35 chars
assert 34 <= len(addr) <= 36, f"unexpected Zcash addr length {len(addr)}"
# ─────────────────────────────────────────────────────────────────────────────
# Cardano — verified against pycardano AND the official `cardano-address` CLI
# ─────────────────────────────────────────────────────────────────────────────
def test_cardano_matches_pycardano() -> None:
"""Our Cardano address matches pycardano (which matches the IOHK reference)."""
addr = cardano_address(TEST_MNEMONIC, network="mainnet")
# Reference: pycardano
from pycardano import HDWallet, Address, Network, PaymentVerificationKey, StakeVerificationKey
wallet = HDWallet.from_mnemonic(TEST_MNEMONIC)
acc = wallet.derive(1852, hardened=True).derive(1815, hardened=True).derive(0, hardened=True)
pay_w = acc.derive(0).derive(0)
stk_w = acc.derive(2).derive(0)
pay_vkey = PaymentVerificationKey.from_primitive(pay_w.public_key)
stk_vkey = StakeVerificationKey.from_primitive(stk_w.public_key)
expected = str(Address(pay_vkey.hash(), stk_vkey.hash(), network=Network.MAINNET))
assert addr == expected, f"got {addr}, pycardano says {expected}"
assert addr.startswith("addr1"), f"expected mainnet prefix, got {addr[:5]}"
assert len(addr) == 103, f"expected 103 chars, got {len(addr)}"
def test_cardano_testnet() -> None:
"""Testnet address uses 'addr_test' HRP and header 0x00."""
addr = cardano_address(TEST_MNEMONIC, network="testnet")
assert addr.startswith("addr_test1"), f"expected testnet prefix, got {addr[:10]}"
def test_cardano_enterprise_matches_pycardano() -> None:
"""Enterprise address (payment only, no stake)."""
addr = cardano_enterprise_address(TEST_MNEMONIC, network="mainnet")
from pycardano import HDWallet, Address, Network, PaymentVerificationKey
wallet = HDWallet.from_mnemonic(TEST_MNEMONIC)
acc = wallet.derive(1852, hardened=True).derive(1815, hardened=True).derive(0, hardened=True)
pay_w = acc.derive(0).derive(0)
pay_vkey = PaymentVerificationKey.from_primitive(pay_w.public_key)
expected = str(Address(pay_vkey.hash(), network=Network.MAINNET))
assert addr == expected
# Enterprise address is shorter (~62 chars)
assert len(addr) < 80
# ─────────────────────────────────────────────────────────────────────────────
# Cross-SDK consistency — generate everything once, log the table
# ─────────────────────────────────────────────────────────────────────────────
def test_print_all_vectors_for_documentation(capsys):
"""Print every address to stdout. Useful for regenerating ADDRESS_GENERATION.md."""
print("\n\n=== WalletPress Address Vectors (golden) ===")
print(f"Mnemonic: {TEST_MNEMONIC}\n")
pub_secp_atom = _secp_pub("m/44'/118'/0'/0/0")
pub_secp_evmos = _secp_pub("m/44'/60'/0'/0/0")
print(f" atom (cosmos) : {cosmos_address(pub_secp_atom, 'cosmos')}")
print(f" osmo (osmo) : {cosmos_address(pub_secp_atom, 'osmo')}")
print(f" juno (juno) : {cosmos_address(pub_secp_atom, 'juno')}")
print(f" sei (sei) : {cosmos_address(pub_secp_atom, 'sei')}")
print(f" evmos (evmos) : {cosmos_address(pub_secp_evmos, 'evmos')}")
print(f" inj (inj) : {cosmos_address(pub_secp_evmos, 'inj')}")
ed_xlm = _ed_pub_from_priv(_ed_priv("m/44'/148'/0'/0'"))
print(f" xlm (stellar) : {stellar_address(ed_xlm)}")
xrp_path = "m/44'/144'/0/0/0"
print(f" xrp (ripple) : {xrp_address(_secp_pub(xrp_path))}")
xtz_priv = _ed_priv("m/44'/1729'/0'/0'")
print(f" xtz (tezos tz1) : {tezos_tz1_address(_ed_pub_from_priv(xtz_priv))}")
_ed_pub_from_priv(_ed_priv("m/44'/396'/0'/0'"))
ton_priv = _ed_priv("m/44'/396'/0'/0'")
print(f" ton (v4r2) : {ton_address_v4r2(_ed_pub_from_priv(ton_priv))}")
fil_path = "m/44'/461'/0/0/0"
print(f" fil (f1) : {filecoin_f1_address(_secp_pub(fil_path))}")
_ed_pub_from_priv(_ed_priv("m/44'/165'/0'"))
nano_priv = _ed_priv("m/44'/165'/0'")
print(f" xno (nano) : {nano_address(_ed_pub_from_priv(nano_priv))}")
_ed_pub_from_priv(_ed_priv("m/44'/283'/0'/0'"))
algo_priv = _ed_priv("m/44'/283'/0'/0'")
print(f" algo (algorand) : {algorand_address(_ed_pub_from_priv(algo_priv))}")
seed32 = hashlib.sha256(_seed()).hexdigest()[:64]
print(f" xmr (monero) : {monero_address(seed32)}")
print(f" dot (substrate 0) : {substrate_ss58_address(TEST_MNEMONIC, 0)}")
print(f" ksm (substrate 2) : {substrate_ss58_address(TEST_MNEMONIC, 2)}")
bch_priv = (
Bip32Secp256k1.FromSeed(_seed())
.DerivePath("m/44'/145'/0'/0/0")
.PrivateKey()
.Raw()
.ToBytes()
)
print(f" bch (cashaddr) : {bitcoin_cash_cashaddr(bch_priv)}")