Skip to content

Commit

Permalink
Merge branch 'feature/oct-399-allocation-nonce' into 'develop'
Browse files Browse the repository at this point in the history
OCT-399 Allocations: add nonce

See merge request wildland/governance/octant!725
  • Loading branch information
aziolek committed Sep 22, 2023
2 parents f1fe956 + 1c90c93 commit 350817f
Show file tree
Hide file tree
Showing 27 changed files with 475 additions and 113 deletions.
32 changes: 25 additions & 7 deletions backend/app/controllers/allocations.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
from typing import List, Dict

from app import database
from app import database, exceptions
from app.controllers import rewards
from app.core.allocations import (
AllocationRequest,
recover_user_address,
deserialize_payload,
verify_allocations,
add_allocations_to_db,
next_allocation_nonce,
)
from app.core.common import AccountFunds
from app.extensions import db, epochs


def allocate(request: AllocationRequest) -> str:
user_address = recover_user_address(request)
user_allocations = deserialize_payload(request.payload)
nonce, user_allocations = deserialize_payload(request.payload)
epoch = epochs.get_pending_epoch()
verify_allocations(epoch, user_address, user_allocations)

user = database.user.get_by_address(user_address)
expected_nonce = next_allocation_nonce(user)
if nonce != expected_nonce:
raise exceptions.WrongAllocationsNonce(nonce, expected_nonce)

user.allocation_nonce = nonce

add_allocations_to_db(
epoch, user_address, user_allocations, request.override_existing_allocations
epoch,
user_address,
nonce,
user_allocations,
request.override_existing_allocations,
)

db.session.commit()

return user_address


def simulate_allocation(payload: Dict, user_address: str):
user_allocations = deserialize_payload(payload)
nonce, user_allocations = deserialize_payload(payload)
epoch = epochs.get_pending_epoch()
verify_allocations(epoch, user_address, user_allocations)
add_allocations_to_db(epoch, user_address, user_allocations, True)
add_allocations_to_db(epoch, user_address, nonce, user_allocations, True)
proposal_rewards = rewards.get_proposals_rewards(epoch)

db.session.rollback()
Expand All @@ -40,7 +53,7 @@ def simulate_allocation(payload: Dict, user_address: str):


def get_all_by_user_and_epoch(
user_address: str, epoch: int = None
user_address: str, epoch: int | None = None
) -> List[AccountFunds]:
epoch = epochs.get_pending_epoch() if epoch is None else epoch

Expand All @@ -61,6 +74,11 @@ def get_all_by_proposal_and_epoch(
return [AccountFunds(a.user.address, a.amount) for a in allocations]


def get_sum_by_epoch(epoch: int = None) -> int:
def get_sum_by_epoch(epoch: int | None = None) -> int:
epoch = epochs.get_pending_epoch() if epoch is None else epoch
return database.allocations.get_alloc_sum_by_epoch(epoch)


def get_allocation_nonce(user_address: str) -> int:
user = database.user.get_by_address(user_address)
return next_allocation_nonce(user)
20 changes: 16 additions & 4 deletions backend/app/core/allocations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List, Dict
from typing import List, Dict, Tuple

from dataclass_wizard import JSONWizard

Expand All @@ -9,6 +9,8 @@
from app.crypto.eip712 import recover_address, build_allocations_eip712_data
from app.extensions import proposals

from app.database.models import User


@dataclass(frozen=True)
class Allocation(JSONWizard):
Expand All @@ -26,6 +28,7 @@ class AllocationRequest:
def add_allocations_to_db(
epoch: int,
user_address: str,
nonce: int,
allocations: List[Allocation],
delete_existing_user_epoch_allocations: bool,
):
Expand All @@ -36,19 +39,20 @@ def add_allocations_to_db(
if delete_existing_user_epoch_allocations:
database.allocations.soft_delete_all_by_epoch_and_user_id(epoch, user.id)

database.allocations.add_all(epoch, user.id, allocations)
database.allocations.add_all(epoch, user.id, nonce, allocations)


def recover_user_address(request: AllocationRequest) -> str:
eip712_data = build_allocations_eip712_data(request.payload)
return recover_address(eip712_data, request.signature)


def deserialize_payload(payload) -> List[Allocation]:
return [
def deserialize_payload(payload) -> Tuple[int, List[Allocation]]:
allocations = [
Allocation.from_dict(allocation_data)
for allocation_data in payload["allocations"]
]
return payload["nonce"], allocations


def verify_allocations(epoch: int, user_address: str, allocations: List[Allocation]):
Expand Down Expand Up @@ -84,3 +88,11 @@ def verify_allocations(epoch: int, user_address: str, allocations: List[Allocati

if proposals_sum > user_budget:
raise exceptions.RewardsBudgetExceeded


def next_allocation_nonce(user: User | None) -> int:
if user is None:
return 0
if user.allocation_nonce is None:
return 0
return user.allocation_nonce + 1
3 changes: 2 additions & 1 deletion backend/app/database/allocations.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,14 @@ def get_alloc_sum_by_epoch(epoch: int) -> int:
return sum([int(a.amount) for a in allocations])


def add_all(epoch: int, user_id: int, allocations):
def add_all(epoch: int, user_id: int, nonce: int, allocations):
now = datetime.utcnow()

new_allocations = [
Allocation(
epoch=epoch,
user_id=user_id,
nonce=nonce,
proposal_address=to_checksum_address(a.proposal_address),
amount=str(a.amount),
created_at=now,
Expand Down
7 changes: 6 additions & 1 deletion backend/app/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ class User(BaseModel):

id = Column(db.Integer, primary_key=True)
address = Column(db.String(42), unique=True, nullable=False)
allocation_nonce = Column(db.Integer, nullable=False, default=0)
allocation_nonce = Column(
db.Integer,
nullable=True,
comment="Allocations signing nonce, last used value. Range [0..inf)",
)


class UserConsents(BaseModel):
Expand All @@ -36,6 +40,7 @@ class Allocation(BaseModel):
id = Column(db.Integer, primary_key=True)
epoch = Column(db.Integer, nullable=False)
user_id = Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
nonce = Column(db.Integer, nullable=False)
user = relationship("User", backref=db.backref("allocations", lazy=True))
proposal_address = Column(db.String(42), nullable=False)
amount = Column(db.String, nullable=False)
Expand Down
8 changes: 8 additions & 0 deletions backend/app/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ def __init__(self, address: str):
super().__init__(self.description.format(address), self.code)


class WrongAllocationsNonce(OctantException):
code = 400
description = "Attempt to use wrong value of nonce ({} instead of {}) when signing allocations"

def __init__(self, used: int, expected: int):
super().__init__(self.description.format(used, expected), self.code)


class ExternalApiException(OctantException):
description = "API call to {} failed. Error: {}"
code = 500
Expand Down
1 change: 1 addition & 0 deletions backend/app/infrastructure/graphql/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ def get_epochs():
"""
)

app.logger.debug("[Subgraph] Getting list of all epochs")
data = request_context.graphql_client.execute(query)
return data
27 changes: 26 additions & 1 deletion backend/app/infrastructure/routes/allocations.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
"allocations": fields.List(
fields.Nested(user_allocations_payload_item),
description="User allocation payload",
)
),
"nonce": fields.Integer(
required=True, description="Allocation signature nonce"
),
},
)

Expand Down Expand Up @@ -180,3 +183,25 @@ def get(self, proposal_address: str, epoch: int):
app.logger.debug(f"Proposal donors {donors}")

return donors


allocation_nonce_model = api.model(
"AllocationNonce",
{
"allocation_nonce": fields.Integer(
required=True,
description="Current value of nonce used to sign allocations message. Note: this has nothing to do with Ethereum account nonce!",
),
},
)


@ns.route("/users/<string:user_address>/allocation_nonce")
@ns.doc(
description="Return current value of allocation nonce. It is neeeded to sign allocations.",
)
class AllocationNonce(OctantResource):
@ns.marshal_with(allocation_nonce_model)
@ns.response(200, "User allocations nonce successfully retrieved")
def get(self, user_address: str):
return {"allocation_nonce": allocations.get_allocation_nonce(user_address)}
34 changes: 33 additions & 1 deletion backend/app/infrastructure/routes/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from app.controllers import epochs
from app.extensions import api
from app.infrastructure import OctantResource
from app.infrastructure import OctantResource, graphql

ns = Namespace("epochs", description="Octant epochs")
api.add_namespace(ns)
Expand All @@ -29,3 +29,35 @@ def get(self):
app.logger.debug(f"Current epoch number: {current_epoch}")

return {"currentEpoch": current_epoch}


indexed_epoch_model = api.model(
"IndexedEpoch",
{
"currentEpoch": fields.Integer(
required=True, description="Current epoch number"
),
"indexedEpoch": fields.Integer(
required=True, description="Indexed epoch number"
),
},
)


@ns.route("/indexed")
@ns.doc(description="Returns last indexed epoch number")
class IndexedEpoch(OctantResource):
@ns.marshal_with(indexed_epoch_model)
@ns.response(200, "Current epoch successfully retrieved")
def get(self):
app.logger.debug("Getting current epoch number")
sg_epochs = sorted(graphql.epochs.get_epochs(), key=lambda d: d["epoch"])
app.logger.debug(f"All indexed epochs: {sg_epochs}")
app.logger.debug(f"Last indexed epoch: {sg_epochs[-1:][0]}")
current_epoch = epochs.get_current_epoch()
app.logger.debug(f"Current epoch number: {current_epoch}")

return {
"currentEpoch": current_epoch,
"indexedEpoch": sg_epochs[-1:][0]["epoch"],
}
10 changes: 7 additions & 3 deletions backend/docs/websockets-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ channels:
payload:
type: object
properties:
nonce:
type: integer
description: Allocation signature nonce
allocations:
type: array
items:
Expand All @@ -55,12 +58,13 @@ channels:
examples:
- payload:
payload:
nonce: "0"
allocations:
- proposalAddress: "0x90F79bf6EB2c4f870365E785982E1f101E93b906"
amount: 100
amount: "100"
- proposalAddress: "0xBcd4042DE499D14e55001CcbB24a551F3b954096"
amount: 5000
signature: 8d704f19cde0f1f9d310e57621229b919a8e17187be332c4bd08bf797d0fb50232b4aa30639b741723e647667d87da1af38fd4601600f4d4e2c6f724abea03d61b
amount: "5000"
signature: "8d704f19cde0f1f9d310e57621229b919a8e17187be332c4bd08bf797d0fb50232b4aa30639b741723e647667d87da1af38fd4601600f4d4e2c6f724abea03d61b"

exception:
subscribe:
Expand Down
36 changes: 36 additions & 0 deletions backend/migrations/versions/5be0b6f99ee7_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""empty message
Revision ID: 5be0b6f99ee7
Revises: c4b0243c24d6
Create Date: 2023-08-28 16:27:24.685638
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '5be0b6f99ee7'
down_revision = 'c4b0243c24d6'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.alter_column('allocation_nonce',
existing_type=sa.INTEGER(),
nullable=True)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.alter_column('allocation_nonce',
existing_type=sa.INTEGER(),
nullable=False)

# ### end Alembic commands ###
32 changes: 32 additions & 0 deletions backend/migrations/versions/79532eaf12d0_add_allocation_nonce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add allocation nonce
Revision ID: 79532eaf12d0
Revises: 5be0b6f99ee7
Create Date: 2023-09-06 14:21:25.237635
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '79532eaf12d0'
down_revision = '5be0b6f99ee7'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('allocations', schema=None) as batch_op:
batch_op.add_column(sa.Column('nonce', sa.Integer(), nullable=False))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('allocations', schema=None) as batch_op:
batch_op.drop_column('nonce')

# ### end Alembic commands ###
Loading

0 comments on commit 350817f

Please sign in to comment.