Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/balanced operators #125

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions oracle/networks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from datetime import timedelta

from decouple import Csv, config
from ens.constants import EMPTY_ADDR_HEX
from eth_typing import HexStr
from web3 import Web3

Expand Down Expand Up @@ -69,9 +68,6 @@
"0x0100000000000000000000002296e122c1a20fca3cac3371357bdad3be0df079"
),
ORACLE_PRIVATE_KEY=config("ORACLE_PRIVATE_KEY", default=""),
ORACLE_STAKEWISE_OPERATOR=Web3.toChecksumAddress(
"0x5fc60576b92c5ce5c341c43e3b2866eb9e0cddd1"
),
AWS_BUCKET_NAME=config("AWS_BUCKET_NAME", default="oracle-votes-mainnet"),
AWS_REGION=config("AWS_REGION", default="eu-central-1"),
AWS_ACCESS_KEY_ID=config("AWS_ACCESS_KEY_ID", default=""),
Expand Down Expand Up @@ -142,7 +138,6 @@
"0x0100000000000000000000005c631621b897f467dd6a91855a0bc97d77b78dc0"
),
ORACLE_PRIVATE_KEY=config("ORACLE_PRIVATE_KEY", default=""),
ORACLE_STAKEWISE_OPERATOR=EMPTY_ADDR_HEX,
AWS_BUCKET_NAME=config(
"AWS_BUCKET_NAME",
default="oracle-votes-harbour-mainnet",
Expand Down Expand Up @@ -216,7 +211,6 @@
"0x010000000000000000000000040f15c6b5bfc5f324ecab5864c38d4e1eef4218"
),
ORACLE_PRIVATE_KEY=config("ORACLE_PRIVATE_KEY", default=""),
ORACLE_STAKEWISE_OPERATOR=EMPTY_ADDR_HEX,
AWS_BUCKET_NAME=config("AWS_BUCKET_NAME", default="oracle-votes-goerli"),
AWS_REGION=config("AWS_REGION", default="eu-central-1"),
AWS_ACCESS_KEY_ID=config("AWS_ACCESS_KEY_ID", default=""),
Expand Down Expand Up @@ -287,7 +281,6 @@
"0x0100000000000000000000006dfc9682e3c3263758ad96e2b2ba9822167f81ee"
),
ORACLE_PRIVATE_KEY=config("ORACLE_PRIVATE_KEY", default=""),
ORACLE_STAKEWISE_OPERATOR=EMPTY_ADDR_HEX,
AWS_BUCKET_NAME=config(
"AWS_BUCKET_NAME",
default="oracle-votes-perm-goerli",
Expand Down Expand Up @@ -361,7 +354,6 @@
"0x010000000000000000000000fc9b67b6034f6b306ea9bd8ec1baf3efa2490394"
),
ORACLE_PRIVATE_KEY=config("ORACLE_PRIVATE_KEY", default=""),
ORACLE_STAKEWISE_OPERATOR=EMPTY_ADDR_HEX,
AWS_BUCKET_NAME=config("AWS_BUCKET_NAME", default="oracle-votes-gnosis"),
AWS_REGION=config("AWS_REGION", default="eu-north-1"),
AWS_ACCESS_KEY_ID=config("AWS_ACCESS_KEY_ID", default=""),
Expand Down
4 changes: 2 additions & 2 deletions oracle/oracle/common/graphql_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,12 @@

LAST_VALIDATORS_QUERY = gql(
"""
query getValidators($block_number: Int) {
query getValidators($block_number: Int, $count: Int) {
validators(
block: { number: $block_number }
orderBy: createdAtBlock
orderDirection: desc
first: 1
first: $count
) {
operator {
id
Expand Down
23 changes: 7 additions & 16 deletions oracle/oracle/validators/controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import List, Set
from typing import List

from eth_account.signers.local import LocalAccount
from eth_typing import BlockNumber, HexStr
Expand All @@ -17,8 +17,9 @@
WAD,
)

from .eth1 import get_validators_deposit_root, select_validator
from .eth1 import get_validators_deposit_root
from .types import ValidatorDepositData, ValidatorsVote, ValidatorVotingParameters
from .validator import select_validators

logger = logging.getLogger(__name__)
w3 = Web3()
Expand Down Expand Up @@ -55,20 +56,10 @@ async def process(
# not enough balance to register next validator
return

validators_deposit_data: List[ValidatorDepositData] = []
used_pubkeys: Set[HexStr] = set()
for _ in range(validators_count):
# select next validator
# TODO: implement scoring system based on the operators performance
deposit_data = await select_validator(
block_number=block_number,
used_pubkeys=used_pubkeys,
)
if deposit_data is None:
break

used_pubkeys.add(deposit_data["public_key"])
validators_deposit_data.append(deposit_data)
validators_deposit_data: List[ValidatorDepositData] = await select_validators(
block_number=block_number,
validators_count=validators_count,
)

if not validators_deposit_data:
logger.warning("Run out of validator keys")
Expand Down
103 changes: 24 additions & 79 deletions oracle/oracle/validators/eth1.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Dict, Set, Union
from typing import Dict

from ens.constants import EMPTY_ADDR_HEX
from eth_typing import HexStr
from web3 import Web3
from web3.types import BlockNumber
Expand All @@ -15,76 +14,41 @@
VALIDATOR_REGISTRATIONS_LATEST_INDEX_QUERY,
VALIDATOR_REGISTRATIONS_QUERY,
)
from oracle.oracle.common.ipfs import ipfs_fetch
from oracle.settings import NETWORK, NETWORK_CONFIG
from oracle.settings import NETWORK

from .types import ValidatorDepositData
from .types import Operator


async def select_validator(
block_number: BlockNumber, used_pubkeys: Set[HexStr]
) -> Union[None, ValidatorDepositData]:
"""Selects the next validator to register."""
async def get_operators(block_number: BlockNumber) -> list[Operator]:
"""Fetch list of registered operators"""
result: Dict = await execute_sw_gql_query(
network=NETWORK,
query=OPERATORS_QUERY,
variables=dict(block_number=block_number),
)
operators = result["operators"]
return [
Operator(
id=Web3.toChecksumAddress(x["id"]),
deposit_data_merkle_proofs=x["depositDataMerkleProofs"],
deposit_data_index=int(x["depositDataIndex"]),
)
for x in result["operators"]
]


async def get_last_operators(
block_number: BlockNumber, validators_count: int
) -> list[HexStr]:
"""Fetch last registered validator's operators addresses."""
result: Dict = await execute_sw_gql_query(
network=NETWORK,
query=LAST_VALIDATORS_QUERY,
variables=dict(block_number=block_number),
variables=dict(block_number=block_number, count=validators_count),
)

last_validators = result["validators"]
if last_validators:
last_operator_id = last_validators[0]["operator"]["id"]
index = _find_operator_index(operators, last_operator_id)
if index is not None and index != len(operators) - 1:
operators = operators[index + 1 :] + [operators[index]] + operators[:index]

_move_to_bottom(operators, NETWORK_CONFIG["ORACLE_STAKEWISE_OPERATOR"])

for operator in operators:
merkle_proofs = operator["depositDataMerkleProofs"]
if not merkle_proofs:
continue

operator_address = Web3.toChecksumAddress(operator["id"])
deposit_data_index = int(operator["depositDataIndex"])
deposit_datum = await ipfs_fetch(merkle_proofs)

max_deposit_data_index = len(deposit_datum) - 1
if deposit_data_index > max_deposit_data_index:
continue

selected_deposit_data = deposit_datum[deposit_data_index]
public_key = selected_deposit_data["public_key"]
can_register = public_key not in used_pubkeys and await can_register_validator(
block_number, public_key
)
while deposit_data_index < max_deposit_data_index and not can_register:
# the edge case when the validator was registered in previous merkle root
# and the deposit data is presented in the same.
deposit_data_index += 1
selected_deposit_data = deposit_datum[deposit_data_index]
public_key = selected_deposit_data["public_key"]
can_register = (
public_key not in used_pubkeys
and await can_register_validator(block_number, public_key)
)

if can_register:
return ValidatorDepositData(
operator=operator_address,
public_key=selected_deposit_data["public_key"],
withdrawal_credentials=selected_deposit_data["withdrawal_credentials"],
deposit_data_root=selected_deposit_data["deposit_data_root"],
deposit_data_signature=selected_deposit_data["signature"],
proof=selected_deposit_data["proof"],
)
return None
operators = []
for validator in result["validators"]:
operators.append(validator["operator"]["id"])
return operators


async def can_register_validator(block_number: BlockNumber, public_key: HexStr) -> bool:
Expand All @@ -107,22 +71,3 @@ async def get_validators_deposit_root(block_number: BlockNumber) -> HexStr:
variables=dict(block_number=block_number),
)
return result["validatorRegistrations"][0]["validatorsDepositRoot"]


def _move_to_bottom(operators, operator_id):
if operator_id == EMPTY_ADDR_HEX:
return

index = _find_operator_index(operators, operator_id)
if index is not None:
operators.append(operators.pop(index))


def _find_operator_index(operators, operator_id):
index = None
operator_id = Web3.toChecksumAddress(operator_id)
for i, operator in enumerate(operators):
if Web3.toChecksumAddress(operator["id"]) == operator_id:
index = i
break
return index
2 changes: 1 addition & 1 deletion oracle/oracle/validators/tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def test_process_success(self):
validatorsDepositRoot=vote["validators_deposit_root"]
),
), patch(
"oracle.oracle.validators.eth1.ipfs_fetch",
"oracle.oracle.validators.validator.ipfs_fetch",
side_effect=ipfs_fetch_query(
deposit_data_root=vote["deposit_data"][0]["deposit_data_root"],
public_key=vote["deposit_data"][0]["public_key"],
Expand Down
134 changes: 134 additions & 0 deletions oracle/oracle/validators/tests/test_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from unittest.mock import patch

from web3 import Web3
from web3.types import BlockNumber

from oracle.oracle.tests.factories import faker

from ..types import ValidatorDepositData
from ..validator import select_validators

w3 = Web3()
block_number: BlockNumber = faker.random_int(150000, 250000)


def generate_operator(deposit_data_count, deposit_data_index) -> dict:
return {
"ipfs": [
{
"amount": str(32 * 10**9),
"deposit_data_root": faker.eth_proof(),
"proof": [faker.eth_proof()] * 6,
"public_key": faker.eth_public_key(),
"signature": faker.eth_signature(),
"withdrawal_credentials": faker.eth_address(),
}
for x in range(deposit_data_count)
],
"deposit_data_merkle_proofs": "/ipfs/" + faker.text(max_nb_chars=20),
"deposit_data_index": deposit_data_index,
"id": faker.eth_address(),
}


def _to_validator_deposit_data(operator, deposit_data_index):
return ValidatorDepositData(
operator=operator["id"],
public_key=operator["ipfs"][deposit_data_index]["public_key"],
withdrawal_credentials=operator["ipfs"][deposit_data_index][
"withdrawal_credentials"
],
deposit_data_root=operator["ipfs"][deposit_data_index]["deposit_data_root"],
deposit_data_signature=operator["ipfs"][deposit_data_index]["signature"],
proof=operator["ipfs"][deposit_data_index]["proof"],
)


class TestValidatorSelect:
async def _process(self, validators_count, operators, last_operators_ids):
with patch(
"oracle.oracle.validators.validator.can_register_validator",
return_value=True,
), patch(
"oracle.oracle.validators.validator.get_last_operators",
return_value=last_operators_ids,
), patch(
"oracle.oracle.validators.validator.get_operators",
return_value=operators,
), patch(
"oracle.oracle.validators.validator.ipfs_fetch",
side_effect=lambda ipfs_hash: [
operator["ipfs"]
for operator in operators
if operator["deposit_data_merkle_proofs"] == ipfs_hash
][0],
):
return await select_validators(
block_number=faker.random_int(10000000, 15000000),
validators_count=validators_count,
)

async def test_single(self):
operators = [
generate_operator(4, 2),
]
result = await self._process(
validators_count=1, operators=operators, last_operators_ids=[]
)
assert result == [_to_validator_deposit_data(operators[0], 2)]

async def test_none(self):
operators = [
generate_operator(2, 4),
]
result = await self._process(
validators_count=1, operators=operators, last_operators_ids=[]
)
assert result == []

async def test_single_several(self):
operators = [
generate_operator(50, 2),
]
result = await self._process(
validators_count=3, operators=operators, last_operators_ids=[]
)
assert result == [
_to_validator_deposit_data(operators[0], 2),
_to_validator_deposit_data(operators[0], 3),
_to_validator_deposit_data(operators[0], 4),
]

async def test_basic_1(self):
operators = [
generate_operator(50, 2),
generate_operator(50, 2),
]
result = await self._process(
validators_count=2,
operators=operators,
last_operators_ids=[operators[0]["id"]] * 10,
)
assert result == [
_to_validator_deposit_data(operators[1], 2),
_to_validator_deposit_data(operators[1], 3),
]

async def test_basic_2(self):
operators = [
generate_operator(50, 2),
generate_operator(50, 2),
generate_operator(50, 2),
]

result = await self._process(
validators_count=3,
operators=operators,
last_operators_ids=[operators[0]["id"]] * 9 + [operators[1]["id"]] * 5,
)

assert result == [
_to_validator_deposit_data(operators[0], 2),
_to_validator_deposit_data(operators[2], 2),
_to_validator_deposit_data(operators[2], 3),
]
6 changes: 6 additions & 0 deletions oracle/oracle/validators/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ class ValidatorsVote(TypedDict):
validators_deposit_root: HexStr
signature: HexStr
deposit_data: List[ValidatorDepositData]


class Operator(TypedDict):
id: ChecksumAddress
deposit_data_merkle_proofs: str
deposit_data_index: int
Loading