Skip to content

Commit

Permalink
[RELEASE] 0.15.0 (#657)
Browse files Browse the repository at this point in the history
  • Loading branch information
aziolek authored Mar 4, 2025
2 parents 413d51e + 26e2705 commit e0f57c8
Show file tree
Hide file tree
Showing 25 changed files with 910 additions and 72 deletions.
8 changes: 8 additions & 0 deletions backend/app/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,11 @@ class InvalidProjectDetailsInput(OctantException):

def __init__(self):
super().__init__(self.description, self.code)


class GPStampsNotFound(OctantException):
code = 404
description = "GP Stamps not found for user or the user does not exist, try fetching them first"

def __init__(self):
super().__init__(self.description, self.code)
4 changes: 2 additions & 2 deletions backend/app/modules/user/antisybil/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ def _has_guest_stamp_applied_by_gp(score: GPStamps, now: datetime) -> bool:


def _apply_gtc_staking_stamp_nullification(
score: int, stamps: GPStamps, now: datetime
) -> int:
score: float, stamps: GPStamps, now: datetime
) -> float:
"""Take score and stamps as returned by Passport and remove score associated with GTC staking"""

all_stamps = json.loads(stamps.stamps)
Expand Down
6 changes: 6 additions & 0 deletions backend/tests/v2/factories/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from sqlalchemy.ext.asyncio import AsyncSession

from tests.v2.factories.gp_scores import GPStampsFactorySet
from tests.v2.factories.uniqueness_quotients import UniquenessQuotientFactorySet
from tests.v2.factories.allocation_requests import AllocationRequestFactorySet
from tests.v2.factories.allocations import AllocationFactorySet
from tests.v2.factories.projects_details import ProjectsDetailsFactorySet
Expand Down Expand Up @@ -31,6 +33,8 @@ class FactoriesAggregator:
pending_snapshots: PendingEpochSnapshotFactorySet
finalized_snapshots: FinalizedEpochSnapshotFactorySet
patrons: PatronModeEventFactorySet
uniqueness_quotients: UniquenessQuotientFactorySet
gp_stamps: GPStampsFactorySet

def __init__(self, fast_session: AsyncSession):
"""
Expand All @@ -44,3 +48,5 @@ def __init__(self, fast_session: AsyncSession):
self.pending_snapshots = PendingEpochSnapshotFactorySet(fast_session)
self.finalized_snapshots = FinalizedEpochSnapshotFactorySet(fast_session)
self.patrons = PatronModeEventFactorySet(fast_session)
self.uniqueness_quotients = UniquenessQuotientFactorySet(fast_session)
self.gp_stamps = GPStampsFactorySet(fast_session)
65 changes: 65 additions & 0 deletions backend/tests/v2/factories/gp_scores.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from datetime import datetime, timedelta

from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import SubFactory
from sqlalchemy import select

from app.infrastructure.database.models import GPStamps, User
from tests.v2.factories.base import FactorySetBase
from tests.v2.factories.users import UserFactory


class GPStampsFactory(AsyncSQLAlchemyFactory):
class Meta:
model = GPStamps
sqlalchemy_session_persistence = "commit"

user_id = SubFactory(UserFactory)
score = 1.0
expires_at = datetime.now() + timedelta(days=30)
stamps = "[]" # Default empty JSON array as string


class GPStampsFactorySet(FactorySetBase):
_factories = {"gp_stamps": GPStampsFactory}

async def create(
self,
user: User = None,
score: float = None,
expires_at: datetime = None,
stamps: str = None,
) -> GPStamps:
factory_kwargs = {}

if user is not None:
factory_kwargs["user_id"] = user.id
if score is not None:
factory_kwargs["score"] = score
if expires_at is not None:
factory_kwargs["expires_at"] = expires_at
if stamps is not None:
factory_kwargs["stamps"] = stamps

gp_stamps = await GPStampsFactory.create(**factory_kwargs)
return gp_stamps

async def get_or_create(
self,
user: User,
score: float = None,
expires_at: datetime = None,
stamps: str = None,
) -> GPStamps:
gp_stamps = await self.session.scalar(
select(GPStamps).filter(GPStamps.user_id == user.id)
)

if not gp_stamps:
gp_stamps = await self.create(
user=user,
score=score,
expires_at=expires_at,
stamps=stamps,
)
return gp_stamps
56 changes: 56 additions & 0 deletions backend/tests/v2/factories/uniqueness_quotients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from decimal import Decimal
from typing import Optional

from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import SubFactory
from sqlalchemy import select

from app.infrastructure.database.models import UniquenessQuotient, User
from tests.v2.factories.base import FactorySetBase
from tests.v2.factories.users import UserFactory


class UniquenessQuotientFactory(AsyncSQLAlchemyFactory):
class Meta:
model = UniquenessQuotient
sqlalchemy_session_persistence = "commit"

user = SubFactory(UserFactory)
epoch = 1
score = "1.0"


class UniquenessQuotientFactorySet(FactorySetBase):
_factories = {"uniqueness_quotient": UniquenessQuotientFactory}

async def create(
self,
user: Optional[User] = None,
epoch: Optional[int] = None,
score: Optional[Decimal] = None,
) -> UniquenessQuotient:
factory_kwargs = {}

if user is not None:
factory_kwargs["user"] = user
if epoch is not None:
factory_kwargs["epoch"] = epoch
if score is not None:
factory_kwargs["score"] = str(score)

uq = await UniquenessQuotientFactory.create(**factory_kwargs)
return uq

async def get_or_create(
self, user: User, epoch: int, score: Optional[Decimal] = None
) -> UniquenessQuotient:
uq = await self.session.scalar(
select(UniquenessQuotient).filter(
UniquenessQuotient.user_id == user.id,
UniquenessQuotient.epoch == epoch,
)
)

if not uq:
uq = await self.create(user=user, epoch=epoch, score=score)
return uq
170 changes: 170 additions & 0 deletions backend/tests/v2/users/test_antisybil_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from fastapi import FastAPI
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.constants import (
LOW_UQ_SCORE,
MAX_UQ_SCORE,
NULLIFIED_UQ_SCORE,
UQ_THRESHOLD_MAINNET,
)
from v2.uniqueness_quotients.dependencies import get_uq_score_getter
from v2.uniqueness_quotients.services import UQScoreGetter
from tests.v2.factories import FactoriesAggregator


@pytest.mark.asyncio
async def test_get_antisybil_status_normal_user(
fast_app: FastAPI,
fast_client: AsyncClient,
fast_session: AsyncSession,
factories: FactoriesAggregator,
):
"""Should return the antisybil status for a normal user"""
alice = await factories.users.get_or_create_alice()

# Create a gp stamps for the user
expires_at = datetime.now(timezone.utc) + timedelta(days=30)
expires_ts = int(expires_at.timestamp())
await factories.gp_stamps.create(
user=alice,
score=7.0,
expires_at=expires_at,
)

# Override uq_score_getter
fake_uq_score_getter = UQScoreGetter(
session=fast_session,
uq_score_threshold=UQ_THRESHOLD_MAINNET,
max_uq_score=MAX_UQ_SCORE,
low_uq_score=LOW_UQ_SCORE,
null_uq_score=NULLIFIED_UQ_SCORE,
guest_list=set(),
timeout_list=set(),
)
fast_app.dependency_overrides[get_uq_score_getter] = lambda: fake_uq_score_getter

async with fast_client as client:
resp = await client.get(f"user/{alice.address}/antisybil-status")
assert resp.status_code == HTTPStatus.OK
assert resp.json() == {
"status": "Known",
"score": "7.0",
"expiresAt": str(expires_ts),
"isOnTimeOutList": False,
}


@pytest.mark.asyncio
async def test_get_antisybil_status_timeout_list_user(
fast_app: FastAPI,
fast_client: AsyncClient,
fast_session: AsyncSession,
factories: FactoriesAggregator,
):
"""Should return the antisybil status for a user on timeout list"""
bob = await factories.users.get_or_create_bob()

# Create a gp stamps for the user
expires_at = datetime.now(timezone.utc) + timedelta(days=30)
expires_ts = int(expires_at.timestamp())
await factories.gp_stamps.create(
user=bob,
score=100.0, # High score but on timeout list
expires_at=expires_at,
)

# Override uq_score_getter with bob on timeout list
fake_uq_score_getter = UQScoreGetter(
session=fast_session,
uq_score_threshold=UQ_THRESHOLD_MAINNET,
max_uq_score=MAX_UQ_SCORE,
low_uq_score=LOW_UQ_SCORE,
null_uq_score=NULLIFIED_UQ_SCORE,
guest_list=set(),
timeout_list=set([bob.address]),
)
fast_app.dependency_overrides[get_uq_score_getter] = lambda: fake_uq_score_getter

async with fast_client as client:
resp = await client.get(f"user/{bob.address}/antisybil-status")
assert resp.status_code == HTTPStatus.OK
assert resp.json() == {
"status": "Known",
"score": "0.0",
"expiresAt": str(expires_ts),
"isOnTimeOutList": True,
}


@pytest.mark.asyncio
async def test_get_antisybil_status_guest_list_user(
fast_app: FastAPI,
fast_client: AsyncClient,
fast_session: AsyncSession,
factories: FactoriesAggregator,
):
"""Should return the antisybil status for a user on guest list"""
charlie = await factories.users.get_or_create_charlie()

# Create a gp stamps for the user
expires_at = datetime.now(timezone.utc) + timedelta(days=30)
expires_ts = int(expires_at.timestamp())
await factories.gp_stamps.create(
user=charlie,
score=0.0, # Low score but on guest list
expires_at=expires_at,
)

# Override uq_score_getter with charlie on guest list
fake_uq_score_getter = UQScoreGetter(
session=fast_session,
uq_score_threshold=UQ_THRESHOLD_MAINNET,
max_uq_score=MAX_UQ_SCORE,
low_uq_score=LOW_UQ_SCORE,
null_uq_score=NULLIFIED_UQ_SCORE,
guest_list=set([charlie.address]),
timeout_list=set(),
)
fast_app.dependency_overrides[get_uq_score_getter] = lambda: fake_uq_score_getter

async with fast_client as client:
resp = await client.get(f"user/{charlie.address}/antisybil-status")
assert resp.status_code == HTTPStatus.OK
assert resp.json() == {
"status": "Known",
"score": "21.0",
"expiresAt": str(expires_ts),
"isOnTimeOutList": False,
}


@pytest.mark.asyncio
async def test_get_antisybil_status_no_gp_stamps(
fast_app: FastAPI,
fast_client: AsyncClient,
fast_session: AsyncSession,
factories: FactoriesAggregator,
):
"""Should return 404 when user has no GPStamps record"""
# Create user but don't create any GPStamps for them
alice = await factories.users.get_or_create_alice()

# Override uq_score_getter with empty lists
fake_uq_score_getter = UQScoreGetter(
session=fast_session,
uq_score_threshold=UQ_THRESHOLD_MAINNET,
max_uq_score=MAX_UQ_SCORE,
low_uq_score=LOW_UQ_SCORE,
null_uq_score=NULLIFIED_UQ_SCORE,
guest_list=set(),
timeout_list=set(),
)
fast_app.dependency_overrides[get_uq_score_getter] = lambda: fake_uq_score_getter

async with fast_client as client:
resp = await client.get(f"user/{alice.address}/antisybil-status")
assert resp.status_code == HTTPStatus.NOT_FOUND
Loading

0 comments on commit e0f57c8

Please sign in to comment.