Skip to content

Commit

Permalink
add rate limits to tx routes
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Mar 23, 2024
1 parent 13ea22c commit e8bb33b
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ LIGHTNING_RESERVE_FEE_MIN=2000
# Rate limit requests to mint. Make sure that you can see request IPs in the logs.
# You may need to adjust your reverse proxy if you only see requests originating from 127.0.0.1
# MINT_RATE_LIMIT=TRUE
# MINT_RATE_LIMIT_PER_MINUTE=20
# MINT_GLOBAL_RATE_LIMIT_PER_MINUTE=20
12 changes: 9 additions & 3 deletions cashu/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,17 @@ class MintLimits(MintSettings):
mint_rate_limit: bool = Field(
default=False, title="Rate limit", description="IP-based rate limiter."
)
mint_rate_limit_per_minute: int = Field(
mint_global_rate_limit_per_minute: int = Field(
default=60,
gt=0,
title="Global rate limit per minute",
description="Number of requests an IP can make per minute to all endpoints.",
)
mint_transaction_rate_limit_per_minute: int = Field(
default=20,
gt=0,
title="Rate limit per minute",
description="Number of requests an IP can make per minute.",
title="Transaction rate limit per minute",
description="Number of requests an IP can make per minute to transaction endpoints.",
)
mint_max_request_length: int = Field(
default=1000,
Expand Down
39 changes: 39 additions & 0 deletions cashu/mint/limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from fastapi import status
from fastapi.responses import JSONResponse
from loguru import logger
from slowapi import Limiter
from slowapi.util import get_remote_address
from starlette.requests import Request

from ..core.settings import settings


def _rate_limit_exceeded_handler(request: Request, exc: Exception) -> JSONResponse:
remote_address = get_remote_address(request)
logger.warning(
f"Rate limit {settings.mint_global_rate_limit_per_minute}/minute exceeded: {remote_address}"
)
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "Rate limit exceeded."},
)


def get_remote_address_excluding_local(request: Request) -> str:
remote_address = get_remote_address(request)
if remote_address == "127.0.0.1":
return ""
return remote_address


limiter_global = Limiter(
key_func=get_remote_address_excluding_local,
default_limits=[f"{settings.mint_global_rate_limit_per_minute}/minute"],
enabled=settings.mint_rate_limit,
)

limiter = Limiter(
key_func=get_remote_address_excluding_local,
default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"],
enabled=settings.mint_rate_limit,
)
36 changes: 5 additions & 31 deletions cashu/mint/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import FastAPI, status
from fastapi import FastAPI
from fastapi.exception_handlers import (
request_validation_exception_handler as _request_validation_exception_handler,
)
Expand All @@ -9,15 +9,13 @@
from starlette.requests import Request

from ..core.settings import settings
from .limit import _rate_limit_exceeded_handler, limiter_global

if settings.debug_profiling:
from fastapi_profiler import PyInstrumentProfilerMiddleware

if settings.mint_rate_limit:
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware


def add_middlewares(app: FastAPI):
Expand All @@ -34,31 +32,7 @@ def add_middlewares(app: FastAPI):
app.add_middleware(PyInstrumentProfilerMiddleware)

if settings.mint_rate_limit:

def get_remote_address_excluding_local(request: Request) -> str:
remote_address = get_remote_address(request)
if remote_address == "127.0.0.1":
return ""
return remote_address

limiter = Limiter(
key_func=get_remote_address_excluding_local,
default_limits=[f"{settings.mint_rate_limit_per_minute}/minute"],
)
app.state.limiter = limiter

def _rate_limit_exceeded_handler(
request: Request, exc: Exception
) -> JSONResponse:
remote_address = get_remote_address(request)
logger.warning(
f"Rate limit {settings.mint_rate_limit_per_minute}/minute exceeded: {remote_address}"
)
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "Rate limit exceeded."},
)

app.state.limiter = limiter_global
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)

Expand Down
26 changes: 20 additions & 6 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Dict, List

from fastapi import APIRouter
from fastapi import APIRouter, Request
from loguru import logger

from ..core.base import (
Expand Down Expand Up @@ -28,6 +28,7 @@
from ..core.errors import CashuError
from ..core.settings import settings
from ..mint.startup import ledger
from .limit import limiter

router: APIRouter = APIRouter()

Expand Down Expand Up @@ -178,7 +179,10 @@ async def keysets() -> KeysetsResponse:
response_model=PostMintQuoteResponse,
response_description="A payment request to mint tokens of a denomination",
)
async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse:
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def mint_quote(
request: Request, payload: PostMintQuoteRequest
) -> PostMintQuoteResponse:
"""
Request minting of new tokens. The mint responds with a Lightning invoice.
This endpoint can be used for a Lightning invoice UX flow.
Expand All @@ -203,7 +207,8 @@ async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse:
response_model=PostMintQuoteResponse,
response_description="Get an existing mint quote to check its status.",
)
async def get_mint_quote(quote: str) -> PostMintQuoteResponse:
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse:
"""
Get mint quote state.
"""
Expand All @@ -228,7 +233,9 @@ async def get_mint_quote(quote: str) -> PostMintQuoteResponse:
"A list of blinded signatures that can be used to create proofs."
),
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def mint(
request: Request,
payload: PostMintRequest,
) -> PostMintResponse:
"""
Expand All @@ -250,7 +257,10 @@ async def mint(
response_model=PostMeltQuoteResponse,
response_description="Melt tokens for a payment on a supported payment method.",
)
async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse:
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def get_melt_quote(
request: Request, payload: PostMeltQuoteRequest
) -> PostMeltQuoteResponse:
"""
Request a quote for melting tokens.
"""
Expand All @@ -266,7 +276,8 @@ async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse
response_model=PostMeltQuoteResponse,
response_description="Get an existing melt quote to check its status.",
)
async def melt_quote(quote: str) -> PostMeltQuoteResponse:
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def melt_quote(request: Request, quote: str) -> PostMeltQuoteResponse:
"""
Get melt quote state.
"""
Expand Down Expand Up @@ -296,7 +307,8 @@ async def melt_quote(quote: str) -> PostMeltQuoteResponse:
" promises for change."
),
)
async def melt(payload: PostMeltRequest) -> PostMeltResponse:
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def melt(request: Request, payload: PostMeltRequest) -> PostMeltResponse:
"""
Requests tokens to be destroyed and sent out via Lightning.
"""
Expand All @@ -320,7 +332,9 @@ async def melt(payload: PostMeltRequest) -> PostMeltResponse:
"An array of blinded signatures that can be used to create proofs."
),
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def swap(
request: Request,
payload: PostSplitRequest,
) -> PostSplitResponse:
"""
Expand Down
14 changes: 12 additions & 2 deletions cashu/mint/router_deprecated.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Dict, List, Optional

from fastapi import APIRouter
from fastapi import APIRouter, Request
from loguru import logger

from ..core.base import (
Expand Down Expand Up @@ -28,6 +28,7 @@
)
from ..core.errors import CashuError
from ..core.settings import settings
from .limit import limiter
from .startup import ledger

router_deprecated: APIRouter = APIRouter()
Expand Down Expand Up @@ -129,7 +130,10 @@ async def keysets_deprecated() -> KeysetsResponse_deprecated:
),
deprecated=True,
)
async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated:
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def request_mint_deprecated(
request: Request, amount: int = 0
) -> GetMintResponse_deprecated:
"""
Request minting of new tokens. The mint responds with a Lightning invoice.
This endpoint can be used for a Lightning invoice UX flow.
Expand Down Expand Up @@ -157,7 +161,9 @@ async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated
),
deprecated=True,
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def mint_deprecated(
request: Request,
payload: PostMintRequest_deprecated,
hash: Optional[str] = None,
payment_hash: Optional[str] = None,
Expand Down Expand Up @@ -204,7 +210,9 @@ async def mint_deprecated(
),
deprecated=True,
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def melt_deprecated(
request: Request,
payload: PostMeltRequest_deprecated,
) -> PostMeltResponse_deprecated:
"""
Expand Down Expand Up @@ -267,7 +275,9 @@ async def check_fees(
),
deprecated=True,
)
@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute")
async def split_deprecated(
request: Request,
payload: PostSplitRequest_Deprecated,
# ) -> Union[PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated]:
):
Expand Down

0 comments on commit e8bb33b

Please sign in to comment.