Files
neuron-tai/tests/test_wallet_binding_proof.py
Dobromir Popov 9abe83b5f4 feat(alpha): complete hardening backlog
Complete the alpha-hardening Ralph task set, including tracker billing/accounting guards, validator fraud-audit primitives, wallet binding proof support, documentation runbooks, and updated tests.

Verification: .venv/bin/python -m compileall -q packages tests; .venv/bin/python -m pytest -q --tb=short (313 passed, 3 skipped, 1 failed: tests/test_mining_cli.py::test_legacy_start_without_port_uses_next_available_port because meshnet-node pid 1263451 is already listening on port 7000).
2026-07-05 21:47:23 +03:00

183 lines
6.6 KiB
Python

"""C6: wallet binding requires proof of ownership, and rebinding is safe.
Any Bearer key could previously bind any wallet string with no proof of
ownership, and gossip `bind` events overwrote an existing binding directly.
These tests cover: signature-gated binding, rebind protection (with an
admin-override escape hatch), and gossip-safe conflict handling.
"""
import json
import urllib.error
import urllib.request
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshnet_node.wallet import _b58encode
from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
from meshnet_tracker.wallet_proof import binding_message, verify_wallet_signature
HIVE_SECRET = "test-hive-secret"
def _keypair():
priv = Ed25519PrivateKey.generate()
return priv, _b58encode(priv.public_key().public_bytes_raw())
def _sign(priv, api_key: str, wallet: str) -> str:
return priv.sign(binding_message(api_key, wallet)).hex()
# ---------------------------------------------------------------- unit tests
def test_verify_wallet_signature_accepts_valid_and_rejects_forged():
priv, wallet = _keypair()
other_priv, _ = _keypair()
message = binding_message("client-key-1", wallet)
assert verify_wallet_signature(wallet, message, priv.sign(message).hex())
# signed by a different key entirely
assert not verify_wallet_signature(wallet, message, other_priv.sign(message).hex())
# signature for a different api_key can't be replayed
other_message = binding_message("client-key-2", wallet)
assert not verify_wallet_signature(wallet, other_message, priv.sign(message).hex())
assert not verify_wallet_signature(wallet, message, "not-hex")
def test_bind_wallet_rejects_conflicting_rebind_without_admin_override():
ledger = BillingLedger(starting_credit=0.0)
ledger.bind_wallet("key-1", "WalletA")
assert ledger.api_key_for_wallet("WalletA") == "key-1"
event = ledger.bind_wallet("key-2", "WalletA")
assert event.get("rejected") is True
assert ledger.api_key_for_wallet("WalletA") == "key-1" # unchanged
override = ledger.bind_wallet("key-2", "WalletA", admin_override=True)
assert not override.get("rejected")
assert ledger.api_key_for_wallet("WalletA") == "key-2"
def test_gossip_bind_event_cannot_overwrite_existing_binding():
"""A conflicting `bind` event applied via gossip must not clobber."""
leader = BillingLedger(starting_credit=0.0)
leader.bind_wallet("key-1", "WalletA")
follower = BillingLedger(starting_credit=0.0)
follower.bind_wallet("key-9", "WalletA") # follower already has its own binding
events, _ = leader.events_since(0)
applied = follower.apply_events(events)
assert applied == 1 # event was processed (and rejected), not dropped/ignored
# the follower's pre-existing binding is untouched by the conflicting gossip event
assert follower.api_key_for_wallet("WalletA") == "key-9"
# ---------------------------------------------------------- HTTP integration
def _post_json(url: str, payload: dict, headers: dict | None = None) -> dict:
req = urllib.request.Request(
url,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json", **(headers or {})},
method="POST",
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
@pytest.fixture
def tracker():
ledger = BillingLedger(starting_credit=0.0)
server = TrackerServer(billing=ledger, accounts=AccountStore(), hive_secret=HIVE_SECRET)
port = server.start()
yield f"http://127.0.0.1:{port}", ledger
server.stop()
def test_bind_without_signature_is_rejected(tracker):
url, ledger = tracker
_, wallet = _keypair()
with pytest.raises(urllib.error.HTTPError) as exc_info:
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet},
headers={"Authorization": "Bearer client-key-1"},
)
assert exc_info.value.code == 400
assert ledger.api_key_for_wallet(wallet) is None
def test_bind_with_forged_signature_is_rejected(tracker):
url, ledger = tracker
priv, wallet = _keypair()
forger_priv, _ = _keypair()
with pytest.raises(urllib.error.HTTPError) as exc_info:
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(forger_priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
assert exc_info.value.code == 401
assert ledger.api_key_for_wallet(wallet) is None
def test_valid_signature_binds_wallet(tracker):
url, ledger = tracker
priv, wallet = _keypair()
reply = _post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
assert reply == {"wallet": wallet, "bound": True}
assert ledger.api_key_for_wallet(wallet) == "client-key-1"
def test_second_key_cannot_steal_bound_wallet(tracker):
url, ledger = tracker
priv, wallet = _keypair()
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
# key-2 gets the wallet owner's genuine signature over *their* api_key,
# proving they control the private key — still must not steal the binding.
with pytest.raises(urllib.error.HTTPError) as exc_info:
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-2", wallet)},
headers={"Authorization": "Bearer client-key-2"},
)
assert exc_info.value.code == 409
assert ledger.api_key_for_wallet(wallet) == "client-key-1"
def test_admin_session_can_force_rebind(tracker):
url, ledger = tracker
priv, wallet = _keypair()
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
admin = _post_json(
f"{url}/v1/auth/register",
{"email": "admin@example.com", "password": "secret-123"},
)
reply = _post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "api_key": "client-key-2"},
headers={"Authorization": f"Bearer {admin['session_token']}"},
)
assert reply == {"wallet": wallet, "bound": True}
assert ledger.api_key_for_wallet(wallet) == "client-key-2"