git / moonbite-network / tests/security/test_poc_mine_ratelimit.py · raw

"""MAX BOUNTY AUDITOR — regression guard for explorer POST /api/mine.Finding: the explorer's mining relay had NO per-client rate limit, so a singleIP could poll it without bound and lean on the node's PoW budget. It now wearsthe same in-process limiter as /tx/broadcast. These tests assert the SECUREbehaviour — a burst from one client is throttled, while CORS preflight is not —so any regression re-opens the finding as a failing test. No real node is used.Run:  python -m pytest tests/security/test_poc_mine_ratelimit.py -v"""import osimport sysimport pytestfrom flask import Flask_EXPLORER_DIR = os.path.join(    os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),    "explorer",)if _EXPLORER_DIR not in sys.path:    sys.path.insert(0, _EXPLORER_DIR)import api  # noqa: E402import config  # noqa: E402class MiningClient:    """RpcClient stand-in whose address is valid and whose node always mines."""    def is_demo(self):        return False    def validateaddress(self, address):        return {"isvalid": True}    def getblockcount(self):        return 100    def generatetoaddress(self, n, address, maxtries):        return ["ab" * 32]@pytest.fixture()def client(monkeypatch):    monkeypatch.setattr(config, "MINING_ENABLED", True)    monkeypatch.setattr(api, "_client", lambda: MiningClient())    api._rl_hits.clear()    app = Flask(__name__)    app.register_blueprint(api.api)    yield app.test_client()    api._rl_hits.clear()def test_mine_endpoint_is_rate_limited(client):    """10/60 cap: a 15-request burst from one client yields at most 10 successes    and at least one 429 (with a Retry-After hint) — the flood is throttled."""    statuses = []    saw_retry_after = False    for _ in range(15):        r = client.post("/api/mine", json={"address": "moon1validlooking"})        statuses.append(r.status_code)        if r.status_code == 429:            saw_retry_after = saw_retry_after or bool(r.headers.get("Retry-After"))    assert 429 in statuses, "/api/mine is NOT rate limited — regression!"    assert statuses.count(200) <= 10    assert saw_retry_after, "429 response is missing a Retry-After header"def test_mine_cors_preflight_is_not_throttled(client):    """A browser miner sends an OPTIONS preflight before each POST. Preflights    must NOT consume the client's quota, so 20 of them all succeed (204)."""    for _ in range(20):        r = client.open("/api/mine", method="OPTIONS")        assert r.status_code == 204