git / moonbite-network / tests/security/test_invariants.py · raw
"""MAX BOUNTY AUDITOR — Phase 4 invariant / property tests.Two safety properties the whole design rests on: I1 PRICE INTEGRITY: last_price for a pair is non-null IFF at least one swap has been driven to verifier-'settled'. No sequence of public API calls (create / cancel / match / init_swap / report_funding) can mint a price. I2 NON-CUSTODY: no exchange/merchant table stores a coin private key, seed, mnemonic, or preimage-before-reveal. The server holds order intents only.Run: python -m pytest tests/security/test_invariants.py -v"""import hashlibimport importlibimport randomimport pytestimport exchangeimport swap_verifier@pytest.fixture()def fresh_db(tmp_path): exchange._conn = None exchange._DB_PATH = tmp_path / "exchange.db" yield if exchange._conn is not None: exchange._conn.close() exchange._conn = None importlib.reload(exchange)# --------------------------------------------------------------------------- ## I1 — PRICE INTEGRITY under random public-API fuzzing# --------------------------------------------------------------------------- #def test_price_stays_null_under_random_public_operations(fresh_db): """Fuzz the public order/swap surface hard. Because no verifier settlement ever runs here, last_price MUST remain null for every run.""" rng = random.Random(1337) live = [] # (order_id, cancel_token, side, addr) for step in range(4000): op = rng.random() if op < 0.5 or not live: side = rng.choice(("buy", "sell")) addr = f"moon1{rng.randrange(10_000)}" price = f"{rng.choice([0.5, 1.0, 1.5, 2.0]):.2f}" amt = str(rng.randint(1, 10)) try: o = exchange.create_order( side=side, pair="MBITE/LTC", price=price, amount=amt, mbite_address=addr, quote_address=f"ltc1{addr}", ) live.append((o["id"], o["cancel_token"], side, addr)) except ValueError: pass # e.g. per-address open-order cap elif op < 0.75: oid, tok, _, _ = rng.choice(live) try: exchange.cancel_order(oid, tok) except ValueError: pass else: oid, tok, _, _ = rng.choice(live) try: exchange.init_swap( oid, tok, hashlock=hashlib.sha256(str(step).encode()).hexdigest(), base_recipient_pk="02" + "bb" * 32, base_refund_pk="02" + "cc" * 32, quote_recipient_pk="02" + "dd" * 32, quote_refund_pk="02" + "ee" * 32, base_locktime=2_000_000_000, quote_locktime=1_900_000_000, ) except ValueError: pass # THE INVARIANT — checked every single step. assert exchange._last_trade_price("MBITE/LTC") is None, ( f"price minted without settlement at step {step}" )def test_price_moves_only_through_verifier_settlement(fresh_db): """Positive control: the ONLY lever that sets last_price is the verifier marking a swap 'settled' (which flips both orders 'settled').""" buy = exchange.create_order( side="buy", pair="MBITE/LTC", price="1.25", amount="4", mbite_address="moon1buyer", quote_address="ltc1buyer", ) exchange.create_order( side="sell", pair="MBITE/LTC", price="1.25", amount="4", mbite_address="moon1seller", quote_address="ltc1seller", ) swap = exchange.init_swap( buy["id"], buy["cancel_token"], hashlock="ab" * 32, base_recipient_pk="02" + "bb" * 32, base_refund_pk="02" + "cc" * 32, quote_recipient_pk="02" + "dd" * 32, quote_refund_pk="02" + "ee" * 32, base_locktime=2_000_000_000, quote_locktime=1_900_000_000, ) assert exchange._last_trade_price("MBITE/LTC") is None # Only the verifier-owned path can reach 'settled'. exchange.apply_swap_verification(swap["swap_id"], {"status": "settled"}) assert exchange._last_trade_price("MBITE/LTC") == "1.25"# --------------------------------------------------------------------------- ## I2 — NON-CUSTODY: schema never stores coin secrets# --------------------------------------------------------------------------- #_FORBIDDEN = ("privkey", "private_key", "secret_exponent", "seed", "mnemonic", "wif")def _columns(conn, table): return [r[1].lower() for r in conn.execute(f"PRAGMA table_info({table})").fetchall()]def test_exchange_schema_holds_no_coin_secrets(fresh_db): exchange.create_order( side="buy", pair="MBITE/LTC", price="1", amount="1", mbite_address="moon1xaddr", quote_address="ltc1xaddr", ) conn = exchange._connect() tables = [r[0] for r in conn.execute( "SELECT name FROM sqlite_master WHERE type='table'" ).fetchall()] for t in tables: cols = _columns(conn, t) for bad in _FORBIDDEN: assert not any(bad in c for c in cols), f"{t}.{cols} may custody a key ({bad})"def test_swaps_store_public_params_only(fresh_db): """Swap rows carry pubkeys/hashlock/txids (all public) — never a private key. The 'preimage' column is only populated AFTER it is revealed on-chain by the counterparty spending, so storing it leaks nothing the chain doesn't.""" conn = exchange._connect() # Force schema creation. exchange.list_swaps_for_verification() cols = _columns(conn, "swaps") assert "base_recipient_pk" in cols and "hashlock" in cols for bad in _FORBIDDEN: assert not any(bad in c for c in cols)