From c37122eef76583b922a1d9eff7bf3690a3c882ff Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 30 Oct 2024 20:34:53 +0000 Subject: [PATCH 01/51] feat: challenge data generation + formatting --- hardhat.config.js | 3 +- sturdy/base/neuron.py | 18 +-- sturdy/base/validator.py | 4 +- sturdy/mock.py | 5 +- sturdy/pool_registry/__init__.py | 0 sturdy/pool_registry/pool_registry.py | 68 +++++++++ sturdy/pools.py | 112 ++++++++------ sturdy/utils/ethmath.py | 1 + sturdy/utils/misc.py | 1 - sturdy/utils/uids.py | 8 +- sturdy/utils/wandb.py | 16 +- sturdy/validator/forward.py | 1 - .../validator/test_integration_validator.py | 7 +- tests/unit/validator/test_pool_generator.py | 142 ++++++++++-------- tests/unit/validator/test_pool_models.py | 12 +- tests/unit/validator/test_simulator.py | 25 +-- tests/unit/validator/test_validator.py | 1 - 17 files changed, 245 insertions(+), 179 deletions(-) create mode 100644 sturdy/pool_registry/__init__.py create mode 100644 sturdy/pool_registry/pool_registry.py diff --git a/hardhat.config.js b/hardhat.config.js index 090995a..3e79ee4 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -17,8 +17,9 @@ module.exports = { // blockNumber: 20825292, // blockNumber: 20874859 // blockNumber: 20892138 + // blockNumber: 20976304 // latest - blockNumber: 20976304 + blockNumber: 21080765 }, accounts, } diff --git a/sturdy/base/neuron.py b/sturdy/base/neuron.py index c5094b7..5f0b9e5 100644 --- a/sturdy/base/neuron.py +++ b/sturdy/base/neuron.py @@ -90,9 +90,7 @@ def __init__(self, config=None): # The wallet holds the cryptographic key pairs for the miner. if self.config.mock: self.wallet = bt.MockWallet(config=self.config) - self.subtensor = MockSubtensor( - self.config.netuid, n=self.config.mock_n, wallet=self.wallet - ) + self.subtensor = MockSubtensor(self.config.netuid, n=self.config.mock_n, wallet=self.wallet) self.metagraph = MockMetagraph(self.config.netuid, subtensor=self.subtensor) else: self.wallet = bt.wallet(config=self.config) @@ -115,12 +113,10 @@ def __init__(self, config=None): self.step = 0 @abstractmethod - async def forward(self, synapse: bt.Synapse) -> bt.Synapse: - ... + async def forward(self, synapse: bt.Synapse) -> bt.Synapse: ... @abstractmethod - def run(self): - ... + def run(self): ... def sync(self): """ @@ -133,9 +129,7 @@ def sync(self): try: self.resync_metagraph() except Exception as e: - bt.logging.error( - "There was an issue with trying to sync with the metagraph! See Error:" - ) + bt.logging.error("There was an issue with trying to sync with the metagraph! See Error:") bt.logging.error(e) if self.should_set_weights(): @@ -164,9 +158,7 @@ def should_sync_metagraph(self): """ Check if enough epoch blocks have elapsed since the last checkpoint to sync. """ - return ( - self.block - self.metagraph.last_update[self.uid] - ) > self.config.neuron.epoch_length + return (self.block - self.metagraph.last_update[self.uid]) > self.config.neuron.epoch_length def should_set_weights(self) -> bool: # Don't set weights on initialization. diff --git a/sturdy/base/validator.py b/sturdy/base/validator.py index 7d4799d..5ee7648 100644 --- a/sturdy/base/validator.py +++ b/sturdy/base/validator.py @@ -192,9 +192,7 @@ def run(self): sim_penalties = { f"similarity_penalties/uid_{uid}": score for uid, score in self.similarity_penalties.items() } - apys = { - f"apys/uid_{uid}": apy for uid, apy in self.sorted_apys.items() - } + apys = {f"apys/uid_{uid}": apy for uid, apy in self.sorted_apys.items()} axon_times = { f"axon_times/uid_{uid}": axon_time for uid, axon_time in self.sorted_axon_times.items() } diff --git a/sturdy/mock.py b/sturdy/mock.py index f50107a..9f3cf2b 100644 --- a/sturdy/mock.py +++ b/sturdy/mock.py @@ -111,14 +111,11 @@ async def single_axon_response(i, axon): # noqa: ANN202, ARG001 s.dendrite.status_message = "OK" synapse.dendrite.process_time = str(process_time) - if self.custom_allocs: pools = synapse.assets_and_pools["pools"] min_amounts = [pool.borrow_amount for pool in pools.values()] - alloc_values = generate_array_with_sum( - np.random, s.assets_and_pools["total_assets"], min_amounts - ) + alloc_values = generate_array_with_sum(np.random, s.assets_and_pools["total_assets"], min_amounts) contract_addrs = [pool.contract_address for pool in s.assets_and_pools["pools"].values()] allocations = {contract_addrs[i]: alloc_values[i] for i in range(len(s.assets_and_pools["pools"]))} diff --git a/sturdy/pool_registry/__init__.py b/sturdy/pool_registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py new file mode 100644 index 0000000..587f4d9 --- /dev/null +++ b/sturdy/pool_registry/pool_registry.py @@ -0,0 +1,68 @@ +POOL_REGISTRY = { + "Sturdy Redact Aggregator": { + "user_address": "0xcFB23D05f32eA0BE0dBb5078d189Cca89688945E", + "assets_and_pools": { + "pools": { + "0x0669091F451142b3228171aE6aD794cF98288124": { + "pool_type": "STURDY_SILO", + "pool_model_disc": "CHAIN", + "contract_address": "0x0669091F451142b3228171aE6aD794cF98288124", + }, + "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D": { + "pool_type": "STURDY_SILO", + "pool_model_disc": "CHAIN", + "contract_address": "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D", + }, + } + }, + }, + "Sturdy Crvusd Aggregator": { + "user_address": "0x73E4C11B670Ef9C025A030A20b72CB9150E54523", + "assets_and_pools": { + "pools": { + "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227": { + "pool_type": "STURDY_SILO", + "pool_model_disc": "CHAIN", + "contract_address": "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227", + }, + "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b": { + "pool_type": "STURDY_SILO", + "pool_model_disc": "CHAIN", + "contract_address": "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b", + }, + "0x26fe402A57D52c8a323bb6e09f06489C8216aC88": { + "pool_type": "STURDY_SILO", + "pool_model_disc": "CHAIN", + "contract_address": "0x26fe402A57D52c8a323bb6e09f06489C8216aC88", + }, + "0x8dDE9A50a91cc0a5DaBdc5d3931c1AF60408c84D": { + "pool_type": "STURDY_SILO", + "pool_model_disc": "CHAIN", + "contract_address": "0x8dDE9A50a91cc0a5DaBdc5d3931c1AF60408c84D", + }, + } + }, + }, + "Morpho USDC Vaults": { + "user_address": "0x000000000000000000000000000000000000dEaD", + "assets_and_pools": { + "pools": { + "0xd63070114470f685b75B74D60EEc7c1113d33a3D": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0xd63070114470f685b75B74D60EEc7c1113d33a3D", + }, + "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", + }, + "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458", + }, + } + }, + }, +} diff --git a/sturdy/pools.py b/sturdy/pools.py index 75f37c0..10fe612 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -32,6 +32,7 @@ from web3.types import BlockData from sturdy.constants import * +from sturdy.pool_registry.pool_registry import POOL_REGISTRY from sturdy.utils.ethmath import wei_div, wei_mul from sturdy.utils.misc import ( format_num_prec, @@ -325,7 +326,6 @@ class AaveV3DefaultInterestRatePool(ChainBasedPoolModel): _totalVariableDebt = PrivateAttr() _reserveFactor = PrivateAttr() _collateral_amount: int = PrivateAttr() - _collateral_amount: int = PrivateAttr() _total_supplied: int = PrivateAttr() _decimals: int = PrivateAttr() @@ -517,6 +517,7 @@ class VariableInterestSturdySiloStrategy(ChainBasedPoolModel): _silo_strategy_contract: Contract = PrivateAttr() _pair_contract: Contract = PrivateAttr() _rate_model_contract: Contract = PrivateAttr() + _curr_deposit_amount: int = PrivateAttr() _util_prec: int = PrivateAttr() _fee_prec: int = PrivateAttr() @@ -524,8 +525,13 @@ class VariableInterestSturdySiloStrategy(ChainBasedPoolModel): _totalBorrow: Any = PrivateAttr() _current_rate_info = PrivateAttr() _rate_prec: int = PrivateAttr() + _block: BlockData = PrivateAttr() + _decimals: int = PrivateAttr() + _asset: Contract = PrivateAttr() + _user_asset_balance: Contract = PrivateAttr() + _user_total_assets: Contract = PrivateAttr() def __hash__(self) -> int: return hash((self._silo_strategy_contract.address, self._pair_contract)) @@ -574,6 +580,15 @@ def pool_init(self, user_addr: str, web3_provider: Web3) -> None: # noqa: ARG00 self._rate_model_contract = retry_with_backoff(rate_model_contract, address=rate_model_contract_address) self._decimals = retry_with_backoff(self._pair_contract.functions.decimals().call) + erc20_abi_file_path = Path(__file__).parent / "abi/IERC20.json" + erc20_abi_file = erc20_abi_file_path.open() + erc20_abi = json.load(erc20_abi_file) + erc20_abi_file.close() + + asset_address = retry_with_backoff(self._pair_contract.functions.asset().call) + asset_contract = web3_provider.eth.contract(abi=erc20_abi, decode_tuples=True) + self._asset = retry_with_backoff(asset_contract, address=asset_address) + self._initted = True except Exception as e: @@ -599,6 +614,8 @@ def sync(self, user_addr: str, web3_provider: Web3) -> None: self._rate_prec = retry_with_backoff(self._rate_model_contract.functions.RATE_PREC().call) + self._user_asset_balance = retry_with_backoff(self._asset.functions.balanceOf(self.user_address).call) + # last 256 unique calls to this will be cached for the next 60 seconds @ttl_cache(maxsize=256, ttl=60) def supply_rate(self, amount: int) -> int: @@ -970,56 +987,61 @@ def generate_eth_public_key(rng_gen: np.random.RandomState) -> str: return account.address -def generate_assets_and_pools(rng_gen: np.random.RandomState) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools +def generate_assets_and_pools( + rng_gen: np.random.RandomState, web3_provider: Web3 +) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools + selected_challenge_data = rng_gen.choice(list(POOL_REGISTRY.keys())) + + return assets_pools_for_challenge_data(selected_challenge_data, web3_provider) + + +def assets_pools_for_challenge_data( + challenge_data, web3_provider: Web3 +) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools assets_and_pools = {} - pools_list = [ - BasePool( - contract_address=generate_eth_public_key(rng_gen=rng_gen), - pool_type=POOL_TYPES.SYNTHETIC, - base_rate=int(randrange_float(MIN_BASE_RATE, MAX_BASE_RATE, BASE_RATE_STEP, rng_gen=rng_gen)), - base_slope=int(randrange_float(MIN_SLOPE, MAX_SLOPE, SLOPE_STEP, rng_gen=rng_gen)), - kink_slope=int( - randrange_float(MIN_KINK_SLOPE, MAX_KINK_SLOPE, SLOPE_STEP, rng_gen=rng_gen), - ), # kink rate - kicks in after pool hits optimal util rate - optimal_util_rate=int( - randrange_float( - MIN_OPTIMAL_RATE, - MAX_OPTIMAL_RATE, - OPTIMAL_UTIL_STEP, - rng_gen=rng_gen, - ), - ), # optimal util rate - after which the kink slope kicks in - borrow_amount=int( - format_num_prec( - wei_mul( - POOL_RESERVE_SIZE, - int( - randrange_float( - MIN_UTIL_RATE, - MAX_UTIL_RATE, - UTIL_RATE_STEP, - rng_gen=rng_gen, - ), - ), - ), - ), - ), # initial borrowed amount from pool - reserve_size=int(POOL_RESERVE_SIZE), - ) - for _ in range(NUM_POOLS) - ] + selected_assets_and_pools = challenge_data["assets_and_pools"] + selected_pools = selected_assets_and_pools["pools"] + user_address = challenge_data["user_address"] - pools = {str(pool.contract_address): pool for pool in pools_list} + pool_list = [] - minimums = [pool.borrow_amount for pool in pools_list] - min_total = sum(minimums) - assets_and_pools["total_assets"] = int(min_total) + int( - math.floor( - randrange_float(MIN_TOTAL_ASSETS_OFFSET, MAX_TOTAL_ASSETS_OFFSET, TOTAL_ASSETS_OFFSET_STEP, rng_gen=rng_gen), + for pool_dict in selected_pools.values(): + pool = PoolFactory.create_pool( + pool_type=POOL_TYPES._member_map_[pool_dict["pool_type"]], + pool_model_disc=pool_dict["pool_model_disc"], + user_address=user_address, + contract_address=pool_dict["contract_address"], ) - ) + pool_list.append(pool) + + pools = {str(pool.contract_address): pool for pool in pool_list} + + # we assume that the user address is the same across pools (valid) + # and also that the asset contracts are the same across said pools + first_pool = pool_list[0] + total_assets = 0 + + match pool.pool_type: + case POOL_TYPES.STURDY_SILO: + first_pool.sync(user_address, web3_provider) + total_assets = first_pool._user_asset_balance + case _: + pass + + for pool in pools.values(): + total_asset = 0 + match pool.pool_type: + case POOL_TYPES.STURDY_SILO: + pool.sync(user_address, web3_provider) + total_asset += pool._curr_deposit_amount + case _: + pass + + total_assets += total_asset + assets_and_pools["pools"] = pools + assets_and_pools["total_assets"] = total_assets return assets_and_pools diff --git a/sturdy/utils/ethmath.py b/sturdy/utils/ethmath.py index e5278cc..0572011 100644 --- a/sturdy/utils/ethmath.py +++ b/sturdy/utils/ethmath.py @@ -12,5 +12,6 @@ def wei_div(x: int, y: int) -> int: def wei_mul_arrays(x: np.ndarray, y: np.ndarray) -> np.ndarray: return (np.multiply(x, y)) // 1e18 + def wei_div_arrays(x: np.ndarray, y: np.ndarray) -> np.ndarray: return (np.divide(x, y)) * 1e18 diff --git a/sturdy/utils/misc.py b/sturdy/utils/misc.py index e523e2b..fb4a03e 100644 --- a/sturdy/utils/misc.py +++ b/sturdy/utils/misc.py @@ -116,7 +116,6 @@ def borrow_rate(util_rate, pool) -> int: ) - def supply_rate(util_rate, pool) -> int: return wei_mul(util_rate, pool.borrow_rate) diff --git a/sturdy/utils/uids.py b/sturdy/utils/uids.py index 66a65f3..c9e64fa 100644 --- a/sturdy/utils/uids.py +++ b/sturdy/utils/uids.py @@ -4,9 +4,7 @@ from typing import List -def check_uid_availability( - metagraph: "bt.metagraph.Metagraph", uid: int, vpermit_tao_limit: int -) -> bool: +def check_uid_availability(metagraph: "bt.metagraph.Metagraph", uid: int, vpermit_tao_limit: int) -> bool: """Check if uid is available. The UID should be available if it is serving and has less than vpermit_tao_limit stake Args: metagraph (:obj: bt.metagraph.Metagraph): Metagraph object @@ -36,9 +34,7 @@ def get_random_uids(self, k: int, exclude: list[int] = None) -> torch.LongTensor avail_uids = [] for uid in range(self.metagraph.n.item()): - uid_is_available = check_uid_availability( - self.metagraph, uid, self.config.neuron.vpermit_tao_limit - ) + uid_is_available = check_uid_availability(self.metagraph, uid, self.config.neuron.vpermit_tao_limit) uid_is_not_excluded = exclude is None or uid not in exclude if uid_is_available: diff --git a/sturdy/utils/wandb.py b/sturdy/utils/wandb.py index 7d639e6..8d4657c 100644 --- a/sturdy/utils/wandb.py +++ b/sturdy/utils/wandb.py @@ -18,10 +18,7 @@ def init_wandb_miner(self, reinit=False): if self.config.mock: tags.append("mock") - wandb_config = { - key: copy.deepcopy(self.config.get(key, None)) - for key in ("neuron", "reward", "netuid", "wandb") - } + wandb_config = {key: copy.deepcopy(self.config.get(key, None)) for key in ("neuron", "reward", "netuid", "wandb")} if wandb_config["neuron"] is not None: wandb_config["neuron"].pop("full_path", None) @@ -33,11 +30,7 @@ def init_wandb_miner(self, reinit=False): entity=self.config.wandb.entity, config=wandb_config, mode="offline" if self.config.wandb.offline else "online", - dir=( - self.config.neuron.full_path - if self.config.neuron is not None - else "wandb_logs" - ), + dir=(self.config.neuron.full_path if self.config.neuron is not None else "wandb_logs"), tags=tags, notes=self.config.wandb.notes, ) @@ -63,10 +56,7 @@ def init_wandb_validator(self, reinit=False): if self.config.neuron.disable_log_rewards: tags.append("disable_log_rewards") - wandb_config = { - key: copy.deepcopy(self.config.get(key, None)) - for key in ("neuron", "reward", "netuid", "wandb") - } + wandb_config = {key: copy.deepcopy(self.config.get(key, None)) for key in ("neuron", "reward", "netuid", "wandb")} wandb_config["neuron"].pop("full_path", None) self.wandb = wandb.init( diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index c8e53c3..2b030c8 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -73,7 +73,6 @@ async def query_and_score_miners( request_type: REQUEST_TYPES = REQUEST_TYPES.SYNTHETIC, user_address: str = ADDRESS_ZERO, ) -> dict[str, AllocInfo]: - # intialize simulator if request_type == REQUEST_TYPES.ORGANIC: self.simulator.initialize(timesteps=1) diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index d08ddb3..b3f6659 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -27,14 +27,14 @@ def setUpClass(cls) -> None: # simulator with preset seed cls.validator.simulator = Simulator(seed=69) - assets_and_pools = generate_assets_and_pools(np.random.RandomState(seed=420)) # type: ignore[] + assets_and_pools = generate_assets_and_pools(np.random.RandomState(seed=420)) # type: ignore[] cls.assets_and_pools = { "pools": assets_and_pools["pools"], "total_assets": int(1000e18), } - cls.contract_addresses = list(assets_and_pools["pools"].keys()) # type: ignore[] + cls.contract_addresses = list(assets_and_pools["pools"].keys()) # type: ignore[] cls.allocations = { cls.contract_addresses[0]: 100e18, @@ -59,7 +59,8 @@ async def test_query_and_score_miners(self) -> None: # use user-defined generated assets and pools simulator_copy = copy.deepcopy(self.validator.simulator) await query_and_score_miners( - self.validator, assets_and_pools=copy.deepcopy(self.assets_and_pools), + self.validator, + assets_and_pools=copy.deepcopy(self.assets_and_pools), ) simulator_copy.initialize() simulator_copy.init_data( diff --git a/tests/unit/validator/test_pool_generator.py b/tests/unit/validator/test_pool_generator.py index 2443b7d..a0ba015 100644 --- a/tests/unit/validator/test_pool_generator.py +++ b/tests/unit/validator/test_pool_generator.py @@ -1,82 +1,96 @@ +import os import unittest import numpy as np +from dotenv import load_dotenv +from eth_account import Account +from web3 import Web3 from sturdy.constants import * +from sturdy.pool_registry.pool_registry import POOL_REGISTRY from sturdy.pools import ( - BasePoolModel, - generate_assets_and_pools, - generate_initial_allocations_for_pools, + assets_pools_for_challenge_data, ) -from sturdy.utils.ethmath import wei_mul + +load_dotenv() +WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL") class TestPoolAndAllocGeneration(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # runs tests on local mainnet fork at block: 21080765 + cls.w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + assert cls.w3.is_connected() + + cls.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + cls.contract_address = "0x0669091F451142b3228171aE6aD794cF98288124" + # Create a funded account for testing + cls.account = Account.create() + cls.w3.eth.send_transaction( + { + "to": cls.account.address, + "from": cls.w3.eth.accounts[0], + "value": cls.w3.to_wei(200000, "ether"), + } + ) + + cls.snapshot_id = cls.w3.provider.make_request("evm_snapshot", []) # type: ignore[] + print(f"snapshot id: {cls.snapshot_id}") + + @classmethod + def tearDownClass(cls) -> None: + # run this after tests to restore original forked state + w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + + w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + def setUp(self) -> None: + self.snapshot_id = self.w3.provider.make_request("evm_snapshot", []) # type: ignore[] + print(f"snapshot id: {self.snapshot_id}") + + def tearDown(self) -> None: + # Optional: Revert to the original snapshot after each test + print("reverting to original evm snapshot") + self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] + def test_generate_assets_and_pools(self) -> None: # same seed on every test run np.random.seed(69) # run test multiple times to to ensure the number generated are # within the correct ranges - for _ in range(100): - result = generate_assets_and_pools(np.random.RandomState(69)) - - pools: dict[str, BasePoolModel] = result["pools"] - total_borrows = sum([pool.borrow_amount for pool in pools.values()]) - - # Assert total assets - self.assertTrue( - total_borrows + MIN_TOTAL_ASSETS_OFFSET <= result["total_assets"] <= total_borrows + MAX_TOTAL_ASSETS_OFFSET - ) - - # Assert number of pools - self.assertEqual(len(result["pools"]), NUM_POOLS) - - # Assert properties of each pool - for pool_info in result["pools"].values(): - self.assertTrue(hasattr(pool_info, "base_rate")) - self.assertTrue( - MIN_BASE_RATE <= pool_info.base_rate <= MAX_BASE_RATE - ) - - self.assertTrue(hasattr(pool_info, "base_slope")) - self.assertTrue(MIN_SLOPE <= pool_info.base_slope <= MAX_SLOPE) - - self.assertTrue(hasattr(pool_info, "kink_slope")) - self.assertTrue( - MIN_KINK_SLOPE <= pool_info.kink_slope <= MAX_KINK_SLOPE - ) - - self.assertTrue(hasattr(pool_info, "optimal_util_rate")) - self.assertTrue( - MIN_OPTIMAL_RATE - <= pool_info.optimal_util_rate - <= MAX_OPTIMAL_RATE - ) - - self.assertTrue(hasattr(pool_info, "reserve_size")) - self.assertEqual(pool_info.reserve_size, POOL_RESERVE_SIZE) - - self.assertTrue(hasattr(pool_info, "borrow_amount")) - self.assertTrue( - wei_mul(MIN_UTIL_RATE, POOL_RESERVE_SIZE) - <= pool_info.borrow_amount - <= wei_mul(MAX_UTIL_RATE, POOL_RESERVE_SIZE) - ) - - def test_generate_initial_allocations_for_pools(self) -> None: - # same seed on every test run - np.random.seed(69) - # run test multiple times to to ensure the number generated are - # within the correct ranges - for _ in range(100): - assets_and_pools = generate_assets_and_pools(np.random.RandomState(69)) - max_alloc = assets_and_pools["total_assets"] - pools = assets_and_pools["pools"] - result = generate_initial_allocations_for_pools(assets_and_pools) - result = dict(result.items()) - - # Assert number of allocations - self.assertEqual(len(result), len(pools)) + keys = list(POOL_REGISTRY.keys())[:2] + for idx in range(2): + key = keys[idx] + challenge_data = POOL_REGISTRY[key] + generated = assets_pools_for_challenge_data(challenge_data, self.w3) + print(generated) + + # check the member variables of the returned value + self.assertEqual(list(generated["pools"].keys()), list(challenge_data["assets_and_pools"]["pools"].keys())) + # check returned total assets + self.assertGreater(generated["total_assets"], 0) if __name__ == "__main__": diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 1a39f0b..4f79943 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -79,7 +79,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20976304, + "blockNumber": 21080765, }, }, ], @@ -268,7 +268,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20976304, + "blockNumber": 21080765, }, }, ], @@ -363,7 +363,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20976304, + "blockNumber": 21080765, }, }, ], @@ -496,7 +496,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20976304, + "blockNumber": 21080765, }, }, ], @@ -582,7 +582,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20976304, + "blockNumber": 21080765, }, }, ], @@ -731,7 +731,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20976304, + "blockNumber": 21080765, }, }, ], diff --git a/tests/unit/validator/test_simulator.py b/tests/unit/validator/test_simulator.py index b8dec87..d540ad6 100644 --- a/tests/unit/validator/test_simulator.py +++ b/tests/unit/validator/test_simulator.py @@ -59,8 +59,7 @@ def test_update_reserves_with_allocs(self): contract_addresses = [addr for addr in self.simulator.assets_and_pools["pools"]] allocations = { - contract_addresses[i]: self.simulator.assets_and_pools["total_assets"] - / len(init_pools) + contract_addresses[i]: self.simulator.assets_and_pools["total_assets"] / len(init_pools) for i in range(len(init_pools)) } @@ -75,9 +74,7 @@ def test_update_reserves_with_allocs(self): # check init pool_history datapoint new_pool_hist_init = self.simulator.pool_history[0][uid] b_rate_should_be = borrow_rate( - wei_div( - new_pool_hist_init.borrow_amount, new_pool_hist_init.reserve_size - ), + wei_div(new_pool_hist_init.borrow_amount, new_pool_hist_init.reserve_size), new_pool, ) self.assertEqual(reserve_should_be, new_pool_hist_init.reserve_size) @@ -95,9 +92,7 @@ def test_update_reserves_with_allocs_partial(self): contract_addresses = [addr for addr in self.simulator.assets_and_pools["pools"]] - allocs = { - contract_addresses[0]: total_assets / 10 - } # should be 0.1 if total assets is 1 + allocs = {contract_addresses[0]: total_assets / 10} # should be 0.1 if total assets is 1 self.simulator.update_reserves_with_allocs(allocs) @@ -112,9 +107,7 @@ def test_update_reserves_with_allocs_partial(self): # check init pool_history datapoint new_pool_hist_init = self.simulator.pool_history[0][uid] b_rate_should_be = borrow_rate( - wei_div( - new_pool_hist_init.borrow_amount, new_pool_hist_init.reserve_size - ), + wei_div(new_pool_hist_init.borrow_amount, new_pool_hist_init.reserve_size), new_pool, ) self.assertEqual(reserve_should_be, new_pool_hist_init.reserve_size) @@ -205,17 +198,13 @@ def test_sim_run(self): for contract_addr, _ in self.simulator.assets_and_pools["pools"].items(): borrow_amounts = [ - self.simulator.pool_history[T][contract_addr].borrow_amount - for T in range(1, self.simulator.timesteps) + self.simulator.pool_history[T][contract_addr].borrow_amount for T in range(1, self.simulator.timesteps) ] borrow_rates = [ - self.simulator.pool_history[T][contract_addr].borrow_rate - for T in range(1, self.simulator.timesteps) + self.simulator.pool_history[T][contract_addr].borrow_rate for T in range(1, self.simulator.timesteps) ] - self.assertTrue( - borrow_amounts.count(borrow_amounts[0]) < len(borrow_amounts) - ) + self.assertTrue(borrow_amounts.count(borrow_amounts[0]) < len(borrow_amounts)) self.assertTrue(borrow_rates.count(borrow_rates[0]) < len(borrow_rates)) # check if simulation runs the same across "reset()s" diff --git a/tests/unit/validator/test_validator.py b/tests/unit/validator/test_validator.py index 414025e..bb8d5d5 100644 --- a/tests/unit/validator/test_validator.py +++ b/tests/unit/validator/test_validator.py @@ -109,7 +109,6 @@ async def test_get_rewards(self) -> None: print(f"allocs: {allocs}") - rewards_dict = {active_uids[k]: v for k, v in enumerate(list(rewards))} sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] From d96b86a893b75f94ba2d54dc81c1517926936e33 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Fri, 1 Nov 2024 02:02:50 +0000 Subject: [PATCH 02/51] feat: logging allocation info --- db/migrations/20240725003510_allocations.sql | 21 --- db/migrations/20241030231410_alloc_table.sql | 36 +++++ neurons/validator.py | 17 ++- sturdy/base/validator.py | 55 ++++---- sturdy/constants.py | 4 + sturdy/pools.py | 26 ++-- sturdy/validator/forward.py | 75 ++++++---- sturdy/validator/reward.py | 129 +++--------------- sturdy/validator/simulator.py | 4 +- sturdy/validator/sql.py | 51 ++++++- .../validator/test_integration_validator.py | 4 +- tests/unit/validator/test_pool_generator.py | 4 +- tests/unit/validator/test_validator.py | 4 +- 13 files changed, 214 insertions(+), 216 deletions(-) delete mode 100644 db/migrations/20240725003510_allocations.sql create mode 100644 db/migrations/20241030231410_alloc_table.sql diff --git a/db/migrations/20240725003510_allocations.sql b/db/migrations/20240725003510_allocations.sql deleted file mode 100644 index 5345c7b..0000000 --- a/db/migrations/20240725003510_allocations.sql +++ /dev/null @@ -1,21 +0,0 @@ --- migrate:up - -CREATE TABLE allocation_requests ( - request_uid TEXT PRIMARY KEY, - assets_and_pools TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE allocations ( - request_uid TEXT, - miner_uid TEXT, - allocation TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (request_uid, miner_uid), - FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) -); - --- migrate:down - -DROP TABLE allocation_requests; -DROP TABLE allocations; diff --git a/db/migrations/20241030231410_alloc_table.sql b/db/migrations/20241030231410_alloc_table.sql new file mode 100644 index 0000000..3de21e6 --- /dev/null +++ b/db/migrations/20241030231410_alloc_table.sql @@ -0,0 +1,36 @@ +-- migrate:up + +CREATE TABLE IF NOT EXISTS allocation_requests ( + request_uid TEXT PRIMARY KEY, + assets_and_pools TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE active_allocs ( + request_uid TEXT PRIMARY KEY, + active BOOLEAN DEFAULT FALSE, + scoring_period_end TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) +); + +CREATE TABLE IF NOT EXISTS allocations ( + request_uid TEXT, + miner_uid TEXT, + allocation TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (request_uid, miner_uid), + FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) +); + +-- This alter statement adds a new column to the allocations table if it exists +ALTER TABLE allocation_requests +ADD COLUMN request_type TEXT NOT NULL DEFAULT 'SYNTHETIC'; +ALTER TABLE allocations +ADD COLUMN axon_time FLOAT NOT NULL DEFAULT 99999.0; -- large number for now + +-- migrate:down + +DROP TABLE IF EXISTS fulfilled_allocs; +DROP TABLE IF EXISTS allocations; +DROP TABLE IF EXISTS allocation_requests; diff --git a/neurons/validator.py b/neurons/validator.py index d23f37b..281a91b 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -36,6 +36,7 @@ # import base validator class which takes care of most of the boilerplate from sturdy.base.validator import BaseValidatorNeuron +from sturdy.constants import SCORING_PERIOD # Bittensor Validator Template: from sturdy.pools import PoolFactory @@ -242,7 +243,7 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None synapse.assets_and_pools["pools"] = new_pools - result = await query_and_score_miners( + axon_times, result = await query_and_score_miners( core_validator, assets_and_pools=synapse.assets_and_pools, request_type=synapse.request_type, @@ -250,10 +251,20 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None ) request_uuid = uid = str(uuid.uuid4()).replace("-", "") - to_ret = dict(list(result.items())[:body.num_allocs]) + to_ret = dict(list(result.items())[: body.num_allocs]) ret = AllocateAssetsResponse(allocations=to_ret, request_uuid=request_uuid) + to_log = AllocateAssetsResponse(allocations=to_ret, request_uuid=request_uuid) with sql.get_db_connection() as conn: - sql.log_allocations(conn, ret.request_uuid, synapse.assets_and_pools, ret.allocations) + # TODO: make challenge period variable and based on user input + sql.log_allocations( + conn, + to_log.request_uuid, + synapse.assets_and_pools, + to_log.allocations, + axon_times, + REQUEST_TYPES.ORGANIC, + SCORING_PERIOD, + ) return ret diff --git a/sturdy/base/validator.py b/sturdy/base/validator.py index 5ee7648..0fb4724 100644 --- a/sturdy/base/validator.py +++ b/sturdy/base/validator.py @@ -17,25 +17,23 @@ # DEALINGS IN THE SOFTWARE. -import os -import copy -import torch -import asyncio import argparse +import asyncio +import copy +import os import threading -from web3 import Web3 -import bittensor as bt - -from typing import List from traceback import print_exception +import bittensor as bt +import torch +from dotenv import load_dotenv +from web3 import Web3 + from sturdy.base.neuron import BaseNeuron +from sturdy.constants import QUERY_RATE from sturdy.mock import MockDendrite from sturdy.utils.config import add_validator_args -from sturdy.utils.wandb import init_wandb_validator, should_reinit_wandb, reinit_wandb -from sturdy.constants import QUERY_RATE - -from dotenv import load_dotenv +from sturdy.utils.wandb import init_wandb_validator, reinit_wandb, should_reinit_wandb class BaseValidatorNeuron(BaseNeuron): @@ -103,7 +101,7 @@ def __init__(self, config=None) -> None: self.thread: threading.Thread = None self.lock = asyncio.Lock() - def serve_axon(self): + def serve_axon(self) -> None: """Serve axon to enable external connections.""" bt.logging.info("serving ip to chain...") @@ -121,17 +119,15 @@ def serve_axon(self): ) except Exception as e: bt.logging.error(f"Failed to serve Axon with exception: {e}") - pass except Exception as e: bt.logging.error(f"Failed to create Axon initialize with exception: {e}") - pass - async def concurrent_forward(self): + async def concurrent_forward(self) -> None: coroutines = [self.forward() for _ in range(self.config.neuron.num_concurrent_forwards)] await asyncio.gather(*coroutines) - def run(self): + def run(self) -> None: """ Initiates and manages the main loop for the miner on the Bittensor network. The main loop handles graceful shutdown on keyboard interrupts and logs unforeseen errors. @@ -236,13 +232,13 @@ def run(self): bt.logging.error("Error during validation", str(err)) bt.logging.debug(print_exception(type(err), err, err.__traceback__)) - async def run_concurrent_forward(self): + async def run_concurrent_forward(self) -> None: try: await self.concurrent_forward() except Exception as e: bt.logging.error(f"Error in concurrent_forward: {e}") - def run_in_background_thread(self): + def run_in_background_thread(self) -> None: """ Starts the validator's operations in a background thread upon entering the context. This method facilitates the use of the validator in a 'with' statement. @@ -255,7 +251,7 @@ def run_in_background_thread(self): self.is_running = True bt.logging.debug("Started") - def stop_run_thread(self): + def stop_run_thread(self) -> None: """ Stops the validator's operations that are running in the background thread. """ @@ -266,11 +262,11 @@ def stop_run_thread(self): self.is_running = False bt.logging.debug("Stopped") - def __enter__(self): + def __enter__(self) -> "BaseValidatorNeuron": self.run_in_background_thread() return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: """ Stops the validator's background operations upon exiting the context. This method facilitates the use of the validator in a 'with' statement. @@ -296,7 +292,7 @@ def __exit__(self, exc_type, exc_value, traceback): bt.logging.debug("closed wandb connection") bt.logging.success("Validator killed") - def set_weights(self): + def set_weights(self) -> None: """ Sets the validator weights to the metagraph hotkeys based on the scores it has received from the miners. The weights determine the trust and incentive level the validator assigns to miner nodes on the network. @@ -352,7 +348,7 @@ def set_weights(self): else: bt.logging.error("set_weights failed", msg) - def resync_metagraph(self): + def resync_metagraph(self) -> None: """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" bt.logging.info("resync_metagraph()") @@ -384,7 +380,7 @@ def resync_metagraph(self): # Update the hotkeys. self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) - def update_scores(self, rewards: torch.Tensor, uids: list[int]): + def update_scores(self, rewards: torch.Tensor, uids: list[int]) -> None: """Performs exponential moving average on the scores based on the rewards received from the miners.""" # Check if rewards contains NaN values. @@ -394,10 +390,7 @@ def update_scores(self, rewards: torch.Tensor, uids: list[int]): rewards = torch.nan_to_num(rewards, 0) # Check if `uids` is already a tensor and clone it to avoid the warning. - if isinstance(uids, torch.Tensor): - uids_tensor = uids.clone().detach() - else: - uids_tensor = torch.tensor(uids).to(self.device) + uids_tensor = uids.clone().detach() if isinstance(uids, torch.Tensor) else torch.tensor(uids).to(self.device) # Compute forward pass rewards, assumes uids are mutually exclusive. # shape: [ metagraph.n ] @@ -410,7 +403,7 @@ def update_scores(self, rewards: torch.Tensor, uids: list[int]): self.scores: torch.Tensor = alpha * scattered_rewards + (1 - alpha) * self.scores.to(self.device) bt.logging.debug(f"Updated moving avg scores: {self.scores}") - def save_state(self): + def save_state(self) -> None: """Saves the state of the validator to a file.""" bt.logging.info("Saving validator state.") @@ -424,7 +417,7 @@ def save_state(self): self.config.neuron.full_path + "/state.pt", ) - def load_state(self): + def load_state(self) -> None: """Loads the state of the validator from a file.""" bt.logging.info("Loading validator state.") diff --git a/sturdy/constants.py b/sturdy/constants.py index 0db0392..a49597b 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -33,6 +33,10 @@ TOTAL_ALLOC_THRESHOLD = 0.98 + +# TODO: make scoring period variable and random? +SCORING_PERIOD = 120 + # The following constants are for different pool models # Aave RESERVE_FACTOR_START_BIT_POSITION = 64 diff --git a/sturdy/pools.py b/sturdy/pools.py index 10fe612..c392bfa 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -987,22 +987,24 @@ def generate_eth_public_key(rng_gen: np.random.RandomState) -> str: return account.address -def generate_assets_and_pools( - rng_gen: np.random.RandomState, web3_provider: Web3 +def generate_challenge_data( + web3_provider: Web3, + rng_gen: np.random.RandomState = np.random.RandomState() # noqa: B008 ) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools - selected_challenge_data = rng_gen.choice(list(POOL_REGISTRY.keys())) + selected_entry = POOL_REGISTRY[rng_gen.choice(list(POOL_REGISTRY.keys()))] + bt.logging.debug(f"Selected pool registry entry: {selected_entry}") - return assets_pools_for_challenge_data(selected_challenge_data, web3_provider) + return assets_pools_for_challenge_data(selected_entry, web3_provider) def assets_pools_for_challenge_data( - challenge_data, web3_provider: Web3 + selected_entry, web3_provider: Web3 ) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools - assets_and_pools = {} + challenge_data = {} - selected_assets_and_pools = challenge_data["assets_and_pools"] + selected_assets_and_pools = selected_entry["assets_and_pools"] selected_pools = selected_assets_and_pools["pools"] - user_address = challenge_data["user_address"] + user_address = selected_entry["user_address"] pool_list = [] @@ -1040,10 +1042,12 @@ def assets_pools_for_challenge_data( total_assets += total_asset - assets_and_pools["pools"] = pools - assets_and_pools["total_assets"] = total_assets + challenge_data["assets_and_pools"] = {} + challenge_data["assets_and_pools"]["pools"] = pools + challenge_data["assets_and_pools"]["total_assets"] = total_assets + challenge_data["user_address"] = user_address - return assets_and_pools + return challenge_data class YearnV3Vault(ChainBasedPoolModel): diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 2b030c8..775e357 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -17,15 +17,17 @@ # DEALINGS IN THE SOFTWARE. import asyncio -import copy +import uuid from typing import Any import bittensor as bt from web3.constants import ADDRESS_ZERO -from sturdy.constants import QUERY_TIMEOUT +from sturdy.constants import QUERY_TIMEOUT, SCORING_PERIOD +from sturdy.pools import generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo -from sturdy.validator.reward import get_rewards +from sturdy.validator.reward import filter_allocations +from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations async def forward(self) -> Any: @@ -39,7 +41,27 @@ async def forward(self) -> Any: """ # initialize pools and assets - await query_and_score_miners(self) + + challenge_data = generate_challenge_data(web3_provider=self.w3) + request_uuid = str(uuid.uuid4()).replace("-", "") + + axon_times, allocations = await query_and_score_miners( + self, + assets_and_pools=challenge_data["assets_and_pools"], + request_type=REQUEST_TYPES.SYNTHETIC, + user_address=challenge_data["user_address"], + ) + + with get_db_connection() as conn: + log_allocations( + conn, + request_uuid, + challenge_data["assets_and_pools"], + allocations, + axon_times, + REQUEST_TYPES.SYNTHETIC, + SCORING_PERIOD + ) async def query_miner( @@ -72,21 +94,7 @@ async def query_and_score_miners( assets_and_pools: Any = None, request_type: REQUEST_TYPES = REQUEST_TYPES.SYNTHETIC, user_address: str = ADDRESS_ZERO, -) -> dict[str, AllocInfo]: - # intialize simulator - if request_type == REQUEST_TYPES.ORGANIC: - self.simulator.initialize(timesteps=1) - else: - self.simulator.initialize() - - # initialize simulator data - # if there is no "organic" info then generate synthetic info - if assets_and_pools is not None: - self.simulator.init_data(init_assets_and_pools=copy.deepcopy(assets_and_pools)) - else: - self.simulator.init_data() - assets_and_pools = self.simulator.assets_and_pools - +) -> tuple[list, dict[str, AllocInfo]]: # The dendrite client queries the network. # TODO: write custom availability function later down the road active_uids = [str(uid) for uid in range(self.metagraph.n.item()) if self.metagraph.axons[uid].is_serving] @@ -100,28 +108,45 @@ async def query_and_score_miners( user_address=user_address, ) + # query all miners responses = await query_multiple_miners( self, synapse, active_uids, ) + allocations = {uid: responses[idx].allocations for idx, uid in enumerate(active_uids)} # type: ignore[] # Log the results for monitoring purposes. bt.logging.debug(f"Assets and pools: {synapse.assets_and_pools}") bt.logging.debug(f"Received allocations (uid -> allocations): {allocations}") - # Adjust the scores based on responses from miners. - rewards, allocs = get_rewards( + # score previously suggested miner allocations based on how well they are performing now + + # get all the request ids for the pools we should be scoring from the db + active_allocs = [] + with get_db_connection() as conn: + active_allocs = get_active_allocs(conn) + + bt.logging.debug(f"Active allocs: {active_allocs}") + + # before logging latest allocations + # filter them + axon_times, filtered_allocs = filter_allocations( self, query=self.step, uids=active_uids, + active_allocations=active_allocs, responses=responses, assets_and_pools=assets_and_pools, ) - bt.logging.info(f"Scored responses: {rewards}") + # calculate rewards for previous active allocations + + # update the moving average scores of the miners + # int_active_uids = [int(uid) for uid in active_uids] + # self.update_scores(rewards, int_active_uids) + + # TODO: sort the miners' by their current scores and return their respective allocations - int_active_uids = [int(uid) for uid in active_uids] - self.update_scores(rewards, int_active_uids) - return allocs + return axon_times, filtered_allocs diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 8fb9518..38b103d 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -321,13 +321,14 @@ def calculate_aggregate_apy( return int(pct_yield // timesteps) # for simplicity each timestep is a day in the simulator -def get_rewards( +def filter_allocations( self, query: int, # noqa: ARG001 uids: list[str], + active_allocations: list, # row of allocations from database responses: list, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], -) -> tuple[torch.Tensor, dict[str, AllocInfo]]: +) -> dict[str, AllocInfo]: """ Returns a tensor of rewards for the given query and responses. @@ -340,37 +341,10 @@ def get_rewards( - allocs: miner allocations along with their respective yields """ - # maximum yield to scale all rewards by - # total apys of allocations per miner - max_apy = 0 - apys = {} - - init_assets_and_pools = copy.deepcopy(assets_and_pools) - - bt.logging.debug(f"Running simulator for {self.simulator.timesteps} timesteps for each allocation...") - - # TODO: assuming that we are only getting immediate apy for organic chainbasedpool requests - pools_to_scan = cast(dict, init_assets_and_pools["pools"]) - # update reserves given allocations - for pool in pools_to_scan.values(): - match pool.pool_type: - case T if T in ( - POOL_TYPES.AAVE, - POOL_TYPES.DAI_SAVINGS, - POOL_TYPES.COMPOUND_V3, - POOL_TYPES.MORPHO, - POOL_TYPES.YEARN_V3, - ): - pool.sync(self.w3) - case POOL_TYPES.STURDY_SILO: - pool.sync(pool.user_address, self.w3) - case _: - pass + filtered_allocs = {} + axon_times = get_response_times(uids=uids, responses=responses, timeout=QUERY_TIMEOUT) - resulting_apy = 0 for response_idx, response in enumerate(responses): - # reset simulator for next run - self.simulator.reset() allocations = response.allocations @@ -378,102 +352,33 @@ def get_rewards( # is the miner cheating w.r.t allocations? cheating = True try: - cheating = not check_allocations(init_assets_and_pools, allocations) + cheating = not check_allocations(assets_and_pools, allocations) except Exception as e: bt.logging.error(e) # type: ignore[] # score response very low if miner is cheating somehow or returns allocations with incorrect format if cheating: miner_uid = uids[response_idx] - bt.logging.warning(f"CHEATER DETECTED - MINER WITH UID {miner_uid} - PUNISHING 👊😠") - apys[miner_uid] = 0 + bt.logging.warning(f"CHEATER DETECTED | UID {miner_uid}") continue - try: - if response.request_type == REQUEST_TYPES.SYNTHETIC: - # miner does not appear to be cheating - so we init simulator data - self.simulator.init_data( - init_assets_and_pools=copy.deepcopy(init_assets_and_pools), - init_allocations=allocations, - ) - self.simulator.update_reserves_with_allocs() - - self.simulator.run() - - resulting_apy = calculate_aggregate_apy( - allocations, - init_assets_and_pools, - self.simulator.timesteps, - self.simulator.pool_history, - ) - - else: - resulting_apy = calculate_apy( - allocations, - init_assets_and_pools, - ) - except Exception as e: - bt.logging.error(e) # type: ignore[] - bt.logging.error("Failed to calculate apy - PENALIZING MINER") - miner_uid = uids[response_idx] - apys[miner_uid] = 0 - continue - - if resulting_apy > max_apy: - max_apy = resulting_apy - - apys[uids[response_idx]] = resulting_apy - - axon_times = get_response_times(uids=uids, responses=responses, timeout=QUERY_TIMEOUT) - - # set apys for miners that took longer than the timeout to minimum - # TODO: cleaner way to do this? - for uid in uids: - if axon_times[uid] >= QUERY_TIMEOUT: - apys[uid] = 0 - - # TODO: should probably move some things around later down the road - allocs = {} - filtered_allocs = {} - for idx in range(len(responses)): + # used to filter out miners who timed out + # TODO: should probably move some things around later down the road # TODO: cleaner way to do this? - if responses[idx].allocations is None or axon_times[uids[idx]] >= QUERY_TIMEOUT: - allocs[uids[idx]] = { - "apy": 0, - "allocations": None, - } - else: - allocs[uids[idx]] = { - "apy": apys[uids[idx]], - "allocations": responses[idx].allocations, - } - - filtered_allocs[uids[idx]] = { - "apy": apys[uids[idx]], - "allocations": responses[idx].allocations, + if responses.allocations is not None or axon_times[uids[response_idx]] < QUERY_TIMEOUT: + filtered_allocs[uids[response_idx]] = { + "allocations": responses[response_idx].allocations, } - sorted_filtered_allocs = dict(sorted(filtered_allocs.items(), key=lambda item: item[1]["apy"], reverse=True)) - - sorted_apys = dict(sorted(apys.items(), key=lambda item: item[1], reverse=True)) - + curr_filtered_allocs = dict(sorted(filtered_allocs.items(), key=lambda item: int(item[0]))) sorted_axon_times = dict(sorted(axon_times.items(), key=lambda item: item[1])) - bt.logging.debug(f"sorted apys:\n{sorted_apys}") bt.logging.debug(f"sorted axon times:\n{sorted_axon_times}") - bt.logging.debug(f"sorted filtered allocs:\n{sorted_filtered_allocs}") + bt.logging.debug(f"sorted filtered allocs:\n{curr_filtered_allocs}") - self.sorted_apys = sorted_apys self.sorted_axon_times = sorted_axon_times # Get all the reward results by iteratively calling your reward() function. - return ( - _get_rewards( - self, - apys_and_allocations=allocs, - assets_and_pools=init_assets_and_pools, # type: ignore[] - uids=uids, - axon_times=axon_times, - ), - sorted_filtered_allocs, - ) + return axon_times, curr_filtered_allocs + +# def get_rewards() \ No newline at end of file diff --git a/sturdy/validator/simulator.py b/sturdy/validator/simulator.py index 0236672..d5b5ed8 100644 --- a/sturdy/validator/simulator.py +++ b/sturdy/validator/simulator.py @@ -8,7 +8,7 @@ from sturdy.pools import ( BasePoolModel, ChainBasedPoolModel, - generate_assets_and_pools, + generate_challenge_data, generate_initial_allocations_for_pools, ) from sturdy.protocol import AllocationsDict @@ -39,7 +39,7 @@ def init_data( raise RuntimeError("You must have first initialize()-ed the simulation if you'd like to initialize some data") if init_assets_and_pools is None: - self.assets_and_pools: Any = generate_assets_and_pools( + self.assets_and_pools: Any = generate_challenge_data( rng_gen=self.rng_state_container, ) else: diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index b19fa06..30bc829 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -22,7 +22,9 @@ # allocations table ALLOCATION_REQUESTS_TABLE = "allocation_requests" ALLOCATIONS_TABLE = "allocations" +ACTIVE_ALLOCS = "active_allocs" REQUEST_UID = "request_uid" +REQUEST_TYPE = "request_type" MINER_UID = "miner_uid" USER_ADDRESS = "user_address" ALLOCATION = "allocation" @@ -153,33 +155,72 @@ def log_allocations( request_uid: str, assets_and_pools: dict[str, dict[str, PoolModel] | int], allocations: dict[str, AllocInfo], + axon_times: list, + request_type: REQUEST_TYPE, + scoring_period: int, ) -> None: ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + challenge_end = ts_now + scoring_period + scoring_period_end = datetime.fromtimestamp(challenge_end) # noqa: DTZ006 + datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 conn.execute( - f"INSERT INTO {ALLOCATION_REQUESTS_TABLE} VALUES (?, json(?), ?)", + f"INSERT INTO {ALLOCATION_REQUESTS_TABLE} VALUES (?, json(?), ?, ?)", ( request_uid, json.dumps(jsonable_encoder(assets_and_pools)), - datetime.fromtimestamp(ts_now), # noqa: DTZ006 + datetime_now, + request_type, + ), + ) + + conn.execute( + f"INSERT INTO {ACTIVE_ALLOCS} VALUES (?, json(?), ?, ?)", + ( + request_uid, + True, + scoring_period_end, + datetime_now, ), ) to_insert = [] - ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 for miner_uid, miner_allocation in allocations.items(): row = ( request_uid, miner_uid, to_json_string(miner_allocation), - datetime.fromtimestamp(ts_now), # noqa: DTZ006 + datetime_now, + axon_times[miner_uid] ) to_insert.append(row) - conn.executemany(f"INSERT INTO {ALLOCATIONS_TABLE} VALUES (?, ?, json(?), ?)", to_insert) + conn.executemany(f"INSERT INTO {ALLOCATIONS_TABLE} VALUES (?, ?, json(?), ?, ?)", to_insert) conn.commit() +def get_active_allocs( + conn: sqlite3.Connection, +) -> list: + + # TODO: change the logic of handling "active allocations" + # for now we simply get ones which are still in their "challenge" + # period, and consider them to determine the score of miners + # TODO: the existance "active" column may be redundant + query = f""" + SELECT * FROM {ACTIVE_ALLOCS} + WHERE scoring_period_end >= ? + AND active == True + """ + ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 + + cur = conn.execute(query, [datetime_now]) + rows = cur.fetchall() + + return [dict(row) for row in rows] + + def get_filtered_allocations( conn: sqlite3.Connection, request_uid: str | None, diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index b3f6659..4381422 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -5,7 +5,7 @@ import numpy as np from neurons.validator import Validator -from sturdy.pools import generate_assets_and_pools +from sturdy.pools import generate_challenge_data from sturdy.validator.forward import query_and_score_miners from sturdy.validator.simulator import Simulator @@ -27,7 +27,7 @@ def setUpClass(cls) -> None: # simulator with preset seed cls.validator.simulator = Simulator(seed=69) - assets_and_pools = generate_assets_and_pools(np.random.RandomState(seed=420)) # type: ignore[] + assets_and_pools = generate_challenge_data(np.random.RandomState(seed=420)) # type: ignore[] cls.assets_and_pools = { "pools": assets_and_pools["pools"], diff --git a/tests/unit/validator/test_pool_generator.py b/tests/unit/validator/test_pool_generator.py index a0ba015..ee981ee 100644 --- a/tests/unit/validator/test_pool_generator.py +++ b/tests/unit/validator/test_pool_generator.py @@ -83,8 +83,8 @@ def test_generate_assets_and_pools(self) -> None: keys = list(POOL_REGISTRY.keys())[:2] for idx in range(2): key = keys[idx] - challenge_data = POOL_REGISTRY[key] - generated = assets_pools_for_challenge_data(challenge_data, self.w3) + selected_entry = POOL_REGISTRY[key] + generated = assets_pools_for_challenge_data(selected_entry, self.w3) print(generated) # check the member variables of the returned value diff --git a/tests/unit/validator/test_validator.py b/tests/unit/validator/test_validator.py index bb8d5d5..a3afe41 100644 --- a/tests/unit/validator/test_validator.py +++ b/tests/unit/validator/test_validator.py @@ -8,7 +8,7 @@ from neurons.validator import Validator from sturdy.constants import QUERY_TIMEOUT from sturdy.mock import MockDendrite -from sturdy.pools import generate_assets_and_pools +from sturdy.pools import generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocationsDict from sturdy.validator.reward import get_rewards @@ -27,7 +27,7 @@ def setUpClass(cls) -> None: # TODO: this doesn't work? # cls.validator.simulator = Simulator(69) - assets_and_pools = generate_assets_and_pools(np.random.RandomState(seed=420)) + assets_and_pools = generate_challenge_data(np.random.RandomState(seed=420)) cls.contract_addresses: list[str] = list(assets_and_pools["pools"].keys()) # type: ignore[] From b7127c039f8d7cd3f5332136cd0ddb7b05338224 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Fri, 1 Nov 2024 05:49:45 +0000 Subject: [PATCH 03/51] feat: purge simulator & synthetic pool types + fix miner --- demos/plot_simulator.py | 155 ---------------- neurons/validator.py | 34 +--- sturdy/algo.py | 31 ++-- sturdy/pools.py | 120 ++----------- sturdy/protocol.py | 9 +- sturdy/validator/forward.py | 3 +- sturdy/validator/reward.py | 18 +- sturdy/validator/simulator.py | 174 ------------------ sturdy/validator/sql.py | 4 +- tests/unit/validator/test_simulator.py | 235 ------------------------- 10 files changed, 49 insertions(+), 734 deletions(-) delete mode 100644 demos/plot_simulator.py delete mode 100644 sturdy/validator/simulator.py delete mode 100644 tests/unit/validator/test_simulator.py diff --git a/demos/plot_simulator.py b/demos/plot_simulator.py deleted file mode 100644 index 7715637..0000000 --- a/demos/plot_simulator.py +++ /dev/null @@ -1,155 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd - -from sturdy.constants import * -from sturdy.utils.ethmath import wei_div, wei_mul -from sturdy.utils.misc import borrow_rate -from sturdy.validator.simulator import Simulator - -""" -This is a script which can be used to play around with the simulator. -It comes with a function to plot pool borrow rates, etc. over timestamps -""" - - -def plot_simulation_results(simulator) -> None: - borrow_amount_history = [] - borrow_rate_history = [] - utilization_rate_history = [] - supply_rate_history = [] - median_borrow_rate_history = [] - - for t in range(simulator.timesteps): - borrow_amounts = [pool.borrow_amount for pool in simulator.pool_history[t].values()] - reserve_sizes = [pool.reserve_size for pool in simulator.pool_history[t].values()] - borrow_rates = [pool.borrow_rate for pool in simulator.pool_history[t].values()] - utilization_rates = [wei_div(borrow_amounts[i], reserve_sizes[i]) for i in range(len(borrow_amounts))] - supply_rates = [wei_mul(utilization_rates[i], borrow_rates[i]) for i in range(len(borrow_amounts))] - - borrow_amount_history.append(borrow_amounts) - borrow_rate_history.append(borrow_rates) - utilization_rate_history.append(utilization_rates) - supply_rate_history.append(supply_rates) - median_borrow_rate_history.append(np.median(borrow_rates)) - - # Convert data to more manageable format - borrow_amount_history_df = ( - pd.DataFrame(borrow_amount_history, columns=[ - f"Pool_{name[:6]}" for name in simulator.assets_and_pools["pools"] - ]).apply(pd.to_numeric) - / 1e18 - ) - borrow_rate_history_df = ( - pd.DataFrame(borrow_rate_history, columns=[ - f"Pool_{name[:6]}" for name in simulator.assets_and_pools["pools"] - ]).apply(pd.to_numeric) / 1e18 - ) - utilization_rate_history_df = ( - pd.DataFrame( - utilization_rate_history, - columns=[ - f"Pool_{name[:6]}" for name in simulator.assets_and_pools["pools"] - ], - ).apply(pd.to_numeric) - / 1e18 - ) - supply_rate_history_df = ( - pd.DataFrame(supply_rate_history, columns=[ - f"Pool_{name[:6]}" for name in simulator.assets_and_pools["pools"] - ]).apply(pd.to_numeric) - / 1e18 - ) - median_borrow_rate_history_df = ( - pd.Series(median_borrow_rate_history, name="Median Borrow Rate").apply(pd.to_numeric) / 1e18 - ) - - plt.style.use("dark_background") - fig, axs = plt.subplots(3, 2, figsize=(15, 15)) - axs[2, 1].remove() # Remove the subplot in the bottom right corner - axs[2, 0].remove() # Remove the subplot in the bottom left corner - - def save_plot(event): - if event.key == "s": - plt.savefig("simulation_plot.png") - print("Plot saved as 'simulation_plot.png'") - - fig.canvas.mpl_connect("key_press_event", save_plot) - - # Plot borrow rates with median borrow rate - for column in borrow_rate_history_df: - axs[0, 0].plot( - borrow_rate_history_df.index, - borrow_rate_history_df[column], - label=column, - alpha=0.5, - ) - axs[0, 0].plot( - median_borrow_rate_history_df.index, - median_borrow_rate_history_df, - label="Median Borrow Rate", - color="white", - linewidth=2, - linestyle="--", - ) - axs[0, 0].set_title("Simulated Borrow Rates Over Time") - axs[0, 0].set_xlabel("Time Step") - axs[0, 0].set_ylabel("Borrow Rate") - axs[0, 0].legend(title="Pools", bbox_to_anchor=(1.05, 1), loc="upper left") - - # Plot borrow amounts - borrow_amount_history_df.plot(ax=axs[0, 1]) - axs[0, 1].set_title("Simulated Borrow Amounts Over Time") - axs[0, 1].set_xlabel("Time Step") - axs[0, 1].set_ylabel("Borrow Amount") - # axs[0, 1].legend(title="Pools", bbox_to_anchor=(1.05, 1), loc="upper left") - axs[0, 1].get_legend().remove() - - # Plot utilization rates - utilization_rate_history_df.plot(ax=axs[1, 0]) - axs[1, 0].set_title("Simulated Utilization Rates Over Time") - axs[1, 0].set_xlabel("Time Step") - axs[1, 0].set_ylabel("Utilization Rate") - axs[1, 0].legend(title="Pools", bbox_to_anchor=(1.05, 1), loc="upper left") - # axs[1, 0].get_legend().remove() - - # Plot supply rates - supply_rate_history_df.plot(ax=axs[1, 1]) - axs[1, 1].set_title("Simulated Supply Rates Over Time") - axs[1, 1].set_xlabel("Time Step") - axs[1, 1].set_ylabel("Supply Rate") - # axs[1, 1].legend(title="Pools", bbox_to_anchor=(1.05, 1), loc="upper left") - axs[1, 1].get_legend().remove() - - # Create a new axis that spans the entire bottom row - ax_interest_rates = fig.add_subplot(3, 1, 3) - - # Plot interest rate curves for the pools - utilization_range = np.linspace(0, 1, 100) - for pool_addr, pool in simulator.assets_and_pools["pools"].items(): - interest_rates = [borrow_rate(u * 1e18, pool) / 1e18 for u in utilization_range] - ax_interest_rates.plot(utilization_range, interest_rates, label=f"Pool_{pool_addr[:6]}") - - ax_interest_rates.set_title("Interest Rate Curves for the Pools") - ax_interest_rates.set_xlabel("Utilization Rate") - ax_interest_rates.set_ylabel("Borrow Rate") - # ax_interest_rates.legend(title="Pools", bbox_to_anchor=(1.05, 1), loc="upper left") - - # Ensure labels don't overlap and improve layout - plt.tight_layout(rect=[0, 0, 1, 0.96]) - plt.show() - - -# Usage -if __name__ == "__main__": - np.random.seed(69) - num_sims = 10 - for _ in range(num_sims): - sim = Simulator( - seed=np.random.randint(0, 1000), - ) - sim.initialize() - sim.init_data() - sim.run() - - plot_simulation_results(sim) diff --git a/neurons/validator.py b/neurons/validator.py index 281a91b..505c833 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -52,7 +52,6 @@ # api key db from sturdy.validator import forward, query_and_score_miners, sql -from sturdy.validator.simulator import Simulator class Validator(BaseValidatorNeuron): @@ -74,7 +73,6 @@ def __init__(self, config=None) -> None: bt.logging.info("load_state()") self.load_state() self.uid_to_response = {} - self.simulator = Simulator() async def forward(self) -> Any: """ @@ -217,29 +215,15 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None new_pools = {} for uid, pool in pools.items(): - match synapse.request_type: - case REQUEST_TYPES.SYNTHETIC: - new_pool = PoolFactory.create_pool( - pool_type=pool.pool_type, - contract_address=pool.contract_address, - base_rate=pool.base_rate, - base_slope=pool.base_slope, - kink_slope=pool.kink_slope, - optimal_util_rate=pool.optimal_util_rate, - borrow_amount=pool.borrow_amount, - reserve_size=pool.reserve_size, - ) - new_pools[uid] = new_pool - case _: # TODO: We assume this is an "organic request" - new_pool = PoolFactory.create_pool( - pool_type=pool.pool_type, - web3_provider=core_validator.w3, # type: ignore[] - user_address=( - pool.user_address if pool.user_address != ADDRESS_ZERO else synapse.user_address - ), # TODO: is there a cleaner way to do this? - contract_address=pool.contract_address, - ) - new_pools[uid] = new_pool + new_pool = PoolFactory.create_pool( + pool_type=pool.pool_type, + web3_provider=core_validator.w3, # type: ignore[] + user_address=( + pool.user_address if pool.user_address != ADDRESS_ZERO else synapse.user_address + ), # TODO: is there a cleaner way to do this? + contract_address=pool.contract_address, + ) + new_pools[uid] = new_pool synapse.assets_and_pools["pools"] = new_pools diff --git a/sturdy/algo.py b/sturdy/algo.py index 3134eae..456c1e7 100644 --- a/sturdy/algo.py +++ b/sturdy/algo.py @@ -7,11 +7,10 @@ from sturdy.base.miner import BaseMinerNeuron from sturdy.pools import ( POOL_TYPES, - BasePool, PoolFactory, get_minimum_allocation, ) -from sturdy.protocol import REQUEST_TYPES, AllocateAssets +from sturdy.protocol import AllocateAssets THRESHOLD = 0.99 # used to avoid over-allocations @@ -20,21 +19,17 @@ def naive_algorithm(self: BaseMinerNeuron, synapse: AllocateAssets) -> dict: bt.logging.debug(f"received request type: {synapse.request_type}") pools = cast(dict, synapse.assets_and_pools["pools"]) - match synapse.request_type: - case REQUEST_TYPES.ORGANIC: - for uid, pool in pools: - pools[uid] = PoolFactory.create_pool( - pool_type=pool.pool_type, - web3_provider=self.w3, # type: ignore[] - user_address=( - pool.user_address if pool.user_address != ADDRESS_ZERO else synapse.user_address - ), # TODO: is there a cleaner way to do this? - contract_address=pool.contract_address, - ) - case _: # we assume it is a synthetic request - for uid in pools: - pools[uid] = BasePool(**pools[uid].dict()) + for uid, pool in pools.items(): + pools[uid] = PoolFactory.create_pool( + pool_type=pool.pool_type, + web3_provider=self.w3, # type: ignore[] + user_address=( + pool.user_address if pool.user_address != ADDRESS_ZERO else synapse.user_address + ), # TODO: is there a cleaner way to do this? + contract_address=pool.contract_address, + ) + total_assets_available = int(THRESHOLD * synapse.assets_and_pools["total_assets"]) pools = cast(dict, synapse.assets_and_pools["pools"]) @@ -76,10 +71,6 @@ def naive_algorithm(self: BaseMinerNeuron, synapse: AllocateAssets) -> dict: apy = pool.supply_rate() supply_rates[pool.contract_address] = apy supply_rate_sum += apy - case POOL_TYPES.SYNTHETIC: - apy = pool.supply_rate - supply_rates[pool.contract_address] = apy - supply_rate_sum += apy case _: pass diff --git a/sturdy/pools.py b/sturdy/pools.py index c392bfa..3724010 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -35,9 +35,7 @@ from sturdy.pool_registry.pool_registry import POOL_REGISTRY from sturdy.utils.ethmath import wei_div, wei_mul from sturdy.utils.misc import ( - format_num_prec, getReserveFactor, - randrange_float, rayMul, retry_with_backoff, ttl_cache, @@ -45,7 +43,6 @@ class POOL_TYPES(IntEnum): - SYNTHETIC = 0 STURDY_SILO = 1 AAVE = 2 DAI_SAVINGS = 3 @@ -146,94 +143,6 @@ def check_allocations( return True -class BasePoolModel(BaseModel): - """This model will primarily be used for synthetic requests""" - - class Config: - use_enum_values = True # This will use the enum's value instead of the enum itself - smart_union = True - - pool_model_disc: Literal["SYNTHETIC"] = Field(default="SYNTHETIC", description="pool type discriminator") - contract_address: str = Field(..., description='the "contract address" of the pool - used here as a uid') - pool_type: POOL_TYPES | int | str = Field(default=POOL_TYPES.SYNTHETIC, const=True, description="type of pool") - base_rate: int = Field(..., description="base interest rate") - base_slope: int = Field(..., description="base interest rate slope") - kink_slope: int = Field(..., description="kink slope") - optimal_util_rate: int = Field(..., description="optimal utilisation rate") - borrow_amount: int = Field(..., description="borrow amount in wei") - reserve_size: int = Field(..., description="pool reserve size in wei") - - @validator("pool_type", pre=True) - def validator_pool_type(cls, value) -> POOL_TYPES | int | str: - if isinstance(value, POOL_TYPES): - return value - if isinstance(value, int): - return POOL_TYPES(value) - if isinstance(value, str): - try: - return POOL_TYPES[value] - except KeyError: - raise ValueError(f"Invalid enum name: {value}") # noqa: B904 - raise ValueError(f"Invalid value: {value}") - - @root_validator - def check_params(cls, values): # noqa: ANN201 - if not Web3.is_address(values.get("contract_address")): - raise ValueError("pool address is invalid!") - if values.get("base_rate") < 0: - raise ValueError("base rate is negative") - if values.get("base_slope") < 0: - raise ValueError("base slope is negative") - if values.get("kink_slope") < 0: - raise ValueError("kink slope is negative") - if values.get("optimal_util_rate") < 0: - raise ValueError("optimal utilization rate is negative") - if values.get("borrow_amount") < 0: - raise ValueError("borrow amount is negative") - if values.get("reserve_size") < 0: - raise ValueError("reserve size is negative") - return values - - -class BasePool(BasePoolModel): - """This class defines the base pool type - - Args: - contract_address: (str), - base_rate: (int), - base_slope: (int), - kink_slope: (int), - optimal_util_rate: (int), - borrow_amount: (int), - reserve_size: (int), - """ - - @property - def util_rate(self) -> int: - return wei_div(self.borrow_amount, self.reserve_size) - - @property - def borrow_rate(self) -> int: - util_rate = self.util_rate - return ( - self.base_rate + wei_mul(wei_div(util_rate, self.optimal_util_rate), self.base_slope) - if util_rate < self.optimal_util_rate - else self.base_rate - + self.base_slope - + wei_mul( - wei_div( - (util_rate - self.optimal_util_rate), - int(1e18 - self.optimal_util_rate), - ), - self.kink_slope, - ) - ) - - @property - def supply_rate(self) -> int: - return wei_mul(self.util_rate, self.borrow_rate) - - class ChainBasedPoolModel(BaseModel): """This serves as the base model of pools which need to pull data from on-chain @@ -289,10 +198,8 @@ def supply_rate(self, **args: Any) -> int: class PoolFactory: @staticmethod - def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel | BasePoolModel: + def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel: match pool_type: - case POOL_TYPES.SYNTHETIC: - return BasePool(**kwargs) case POOL_TYPES.AAVE: return AaveV3DefaultInterestRatePool(**kwargs) case POOL_TYPES.STURDY_SILO: @@ -989,8 +896,8 @@ def generate_eth_public_key(rng_gen: np.random.RandomState) -> str: def generate_challenge_data( web3_provider: Web3, - rng_gen: np.random.RandomState = np.random.RandomState() # noqa: B008 -) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools + rng_gen: np.random.RandomState = np.random.RandomState(), # noqa: B008 +) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools selected_entry = POOL_REGISTRY[rng_gen.choice(list(POOL_REGISTRY.keys()))] bt.logging.debug(f"Selected pool registry entry: {selected_entry}") @@ -999,7 +906,7 @@ def generate_challenge_data( def assets_pools_for_challenge_data( selected_entry, web3_provider: Web3 -) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools +) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools challenge_data = {} selected_assets_and_pools = selected_entry["assets_and_pools"] @@ -1088,13 +995,14 @@ def supply_rate(self, amount: int) -> int: return retry_with_backoff(self._apr_oracle.functions.getExpectedApr(self.contract_address, delta).call) -# generate intial allocations for pools -def generate_initial_allocations_for_pools(assets_and_pools: dict) -> dict: - total_assets: int = assets_and_pools["total_assets"] - pools: dict[str, BasePool] = assets_and_pools["pools"] - allocs = {} - for pool_uid, pool in pools.items(): - alloc = pool.borrow_amount if pool.pool_type == POOL_TYPES.SYNTHETIC else total_assets // len(pools) - allocs[pool_uid] = alloc +# TODO: remove this? +# # generate intial allocations for pools +# def generate_initial_allocations_for_pools(assets_and_pools: dict) -> dict: +# total_assets: int = assets_and_pools["total_assets"] +# pools: dict[str, Chain] = assets_and_pools["pools"] +# allocs = {} +# for pool_uid, pool in pools.items(): +# alloc = pool.borrow_amount if pool.pool_type == POOL_TYPES.SYNTHETIC else total_assets // len(pools) +# allocs[pool_uid] = alloc - return allocs +# return allocs diff --git a/sturdy/protocol.py b/sturdy/protocol.py index b9d37d2..0b02e49 100644 --- a/sturdy/protocol.py +++ b/sturdy/protocol.py @@ -25,7 +25,7 @@ from web3 import Web3 from web3.constants import ADDRESS_ZERO -from sturdy.pools import BasePoolModel, ChainBasedPoolModel +from sturdy.pools import ChainBasedPoolModel class REQUEST_TYPES(IntEnum): @@ -41,16 +41,13 @@ class AllocInfo(TypedDict): allocations: AllocationsDict | None -PoolModel = Annotated[ChainBasedPoolModel | BasePoolModel, Field(discriminator="pool_model_disc")] - - class AllocateAssetsRequest(BaseModel): class Config: use_enum_values = True smart_union = True request_type: REQUEST_TYPES | int | str = Field(default=REQUEST_TYPES.ORGANIC, description="type of request") - assets_and_pools: dict[str, dict[str, PoolModel] | int] = Field( + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int] = Field( ..., description="pools for miners to produce allocation amounts for - uid -> pool_info", ) @@ -108,7 +105,7 @@ class Config: smart_union = True request_type: REQUEST_TYPES | int | str = Field(default=REQUEST_TYPES.ORGANIC, description="type of request") - assets_and_pools: dict[str, dict[str, PoolModel] | int] = Field( + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int] = Field( ..., description="pools for miners to produce allocation amounts for - uid -> pool_info", ) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 775e357..771289f 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -103,8 +103,7 @@ async def query_and_score_miners( synapse = AllocateAssets( request_type=request_type, - assets_and_pools=self.simulator.assets_and_pools, - allocations=self.simulator.allocations, + assets_and_pools=assets_and_pools, user_address=user_address, ) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 38b103d..e6601fd 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -26,7 +26,7 @@ import torch from sturdy.constants import QUERY_TIMEOUT, SIMILARITY_THRESHOLD -from sturdy.pools import POOL_TYPES, BasePoolModel, ChainBasedPoolModel, check_allocations +from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, check_allocations from sturdy.protocol import REQUEST_TYPES, AllocationsDict, AllocInfo from sturdy.utils.ethmath import wei_div, wei_mul @@ -153,7 +153,7 @@ def get_distance(alloc_a: npt.NDArray, alloc_b: npt.NDArray, total_assets: int) def get_similarity_matrix( apys_and_allocations: dict[str, dict[str, AllocationsDict | int]], - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], ) -> dict[str, dict[str, float]]: """ Calculates the similarity matrix for the allocation strategies of miners using normalized Euclidean distance. @@ -205,7 +205,7 @@ def adjust_rewards_for_plagiarism( self, rewards_apy: torch.Tensor, apys_and_allocations: dict[str, dict[str, AllocationsDict | int]], - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], uids: list, axon_times: dict[str, float], similarity_threshold: float = SIMILARITY_THRESHOLD, @@ -253,7 +253,7 @@ def adjust_rewards_for_plagiarism( def _get_rewards( self, apys_and_allocations: dict[str, dict[str, AllocationsDict | int]], - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], uids: list[str], axon_times: dict[str, float], ) -> torch.Tensor: @@ -274,7 +274,7 @@ def _get_rewards( def calculate_apy( allocations: AllocationsDict, - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], ) -> int: """ Calculates immediate projected yields given intial assets and pools, pool history, and number of timesteps @@ -298,7 +298,7 @@ def calculate_apy( def calculate_aggregate_apy( allocations: AllocationsDict, - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], timesteps: int, pool_history: list[dict[str, Any]], ) -> int: @@ -327,7 +327,7 @@ def filter_allocations( uids: list[str], active_allocations: list, # row of allocations from database responses: list, - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], ) -> dict[str, AllocInfo]: """ Returns a tensor of rewards for the given query and responses. @@ -365,9 +365,9 @@ def filter_allocations( # used to filter out miners who timed out # TODO: should probably move some things around later down the road # TODO: cleaner way to do this? - if responses.allocations is not None or axon_times[uids[response_idx]] < QUERY_TIMEOUT: + if response.allocations is not None or axon_times[uids[response_idx]] < QUERY_TIMEOUT: filtered_allocs[uids[response_idx]] = { - "allocations": responses[response_idx].allocations, + "allocations": response.allocations, } curr_filtered_allocs = dict(sorted(filtered_allocs.items(), key=lambda item: int(item[0]))) diff --git a/sturdy/validator/simulator.py b/sturdy/validator/simulator.py deleted file mode 100644 index d5b5ed8..0000000 --- a/sturdy/validator/simulator.py +++ /dev/null @@ -1,174 +0,0 @@ -import copy -from typing import Any - -import gmpy2 -import numpy as np - -from sturdy.constants import * -from sturdy.pools import ( - BasePoolModel, - ChainBasedPoolModel, - generate_challenge_data, - generate_initial_allocations_for_pools, -) -from sturdy.protocol import AllocationsDict -from sturdy.utils.ethmath import wei_div, wei_mul - - -class Simulator: - def __init__( - self, - reversion_speed: float = REVERSION_SPEED, - seed=None, - ) -> None: - self.reversion_speed = reversion_speed - self.assets_and_pools = {} - self.allocations = {} - self.pool_history = [] - self.init_rng = None - self.rng_state_container: Any = None - self.seed = seed - - # initializes data - by default these are randomly generated - def init_data( - self, - init_assets_and_pools: dict[str, dict[str, ChainBasedPoolModel | BasePoolModel] | int] | None = None, - init_allocations: AllocationsDict | None = None, - ) -> None: - if self.rng_state_container is None or self.init_rng is None: - raise RuntimeError("You must have first initialize()-ed the simulation if you'd like to initialize some data") - - if init_assets_and_pools is None: - self.assets_and_pools: Any = generate_challenge_data( - rng_gen=self.rng_state_container, - ) - else: - self.assets_and_pools = init_assets_and_pools - - if init_allocations is None: - self.allocations = generate_initial_allocations_for_pools( - self.assets_and_pools, - ) - else: - self.allocations = init_allocations - - # initialize pool history - self.pool_history = [ - { - uid: copy.deepcopy(pool) - for uid, pool in self.assets_and_pools["pools"].items() # - }, - ] - - # initialize fresh simulation instance - def initialize(self, timesteps: int | None = None, stochasticity: float | None = None) -> None: - # create fresh rng state - self.init_rng = np.random.RandomState(self.seed) - self.rng_state_container = copy.copy(self.init_rng) - - if timesteps is None: - self.timesteps = self.rng_state_container.choice( - np.arange( - MIN_TIMESTEPS, - MAX_TIMESTEPS + TIMESTEPS_STEP, - TIMESTEPS_STEP, - ), - ) - else: - self.timesteps = timesteps - - if stochasticity is None: - self.stochasticity = self.rng_state_container.choice( - np.arange( - MIN_STOCHASTICITY, - MAX_STOCHASTICITY + STOCHASTICITY_STEP, - STOCHASTICITY_STEP, - ), - ) - else: - self.stochasticity = stochasticity - - self.rng_state_container = copy.copy(self.init_rng) - - # reset sim to initial params for rng - def reset(self) -> None: - if self.rng_state_container is None or self.init_rng is None: - raise RuntimeError( - "You must have first initialize()-ed the simulation if you'd like to reset it", - ) - self.rng_state_container = copy.copy(self.init_rng) - - # update the reserves in the pool with given allocations - def update_reserves_with_allocs(self, allocs=None) -> None: - if len(self.pool_history) <= 0 or len(self.assets_and_pools) <= 0 or len(self.allocations) <= 0: - raise RuntimeError( - "You must first initialize() and init_data() before updating reserves!!!", - ) - - allocations = self.allocations if allocs is None else allocs - - if len(self.pool_history) != 1: - raise RuntimeError( - "You must have first init data for the simulation if you'd like to update reserves", - ) - - for uid, alloc in allocations.items(): - pool = self.assets_and_pools["pools"][uid] - pool_history_start = self.pool_history[0] - pool.reserve_size += int(alloc) - pool.reserve_size = int(pool.reserve_size) - pool_from_history = pool_history_start[uid] - pool_from_history.reserve_size += allocations[uid] - - # initialize pools - # Function to update borrow amounts and other pool params based on reversion rate and stochasticity - def generate_new_pool_data(self) -> dict: - latest_pool_data = self.pool_history[-1] - curr_borrow_rates = np.array([pool.borrow_rate for _, pool in latest_pool_data.items()]) - curr_borrow_amounts = np.array([pool.borrow_amount for _, pool in latest_pool_data.items()]) - curr_reserve_sizes = np.array([pool.reserve_size for _, pool in latest_pool_data.items()]) - optimal_util_rates = np.array([pool.optimal_util_rate for _, pool in latest_pool_data.items()]) - base_slopes = np.array([pool.base_slope for _, pool in latest_pool_data.items()]) - kink_slopes = np.array([pool.kink_slope for _, pool in latest_pool_data.items()]) - - median_rate = np.median(curr_borrow_rates) # Calculate the median borrow rate - noise = self.rng_state_container.normal(0, self.stochasticity * 1e18, len(curr_borrow_rates)) # Add some random noise - rate_changes = (-self.reversion_speed * (curr_borrow_rates - median_rate)) + noise # Mean reversion principle - - new_borrow_amounts = [] - # Update the borrow amounts - for i in range(len(curr_borrow_rates)): - opt_util = optimal_util_rates[i] - curr_util = wei_div(curr_borrow_amounts[i], curr_reserve_sizes[i]) - borrow_delta = 0 - if curr_util < opt_util: - borrow_delta = wei_div(wei_mul(curr_reserve_sizes[i], opt_util), base_slopes[i]) - else: - borrow_delta = wei_div( - wei_mul(curr_reserve_sizes[i], int(1e18) - optimal_util_rates[i]), - kink_slopes[i], - ) - - new_borrow_amount = curr_borrow_amounts[i] + wei_mul(borrow_delta, int(rate_changes[i])) - - new_borrow_amounts.append(new_borrow_amount) - - new_borrow_amounts = np.array(new_borrow_amounts) - - amounts = np.clip(new_borrow_amounts, 0, curr_reserve_sizes) # Ensure borrow amounts do not exceed reserves - pool_uids = list(latest_pool_data.keys()) - - new_pools = [copy.deepcopy(pool) for pool in self.assets_and_pools["pools"].values()] - - for idx, pool in enumerate(new_pools): - pool.borrow_amount = amounts[idx] - - return {pool_uids[uid]: pool for uid, pool in enumerate(new_pools)} - - # run simulation - def run(self) -> None: - if len(self.pool_history) != 1: - raise RuntimeError("You must first initialize() and init_data() before running the simulation!!!") - for _ in range(1, self.timesteps): - new_info = self.generate_new_pool_data() - self.pool_history.append(new_info.copy()) diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 30bc829..deba3c4 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -7,7 +7,7 @@ from fastapi.encoders import jsonable_encoder -from sturdy.protocol import AllocInfo, PoolModel +from sturdy.protocol import AllocInfo, ChainBasedPoolModel BALANCE = "balance" KEY = "key" @@ -153,7 +153,7 @@ def to_json_string(input_data) -> str: def log_allocations( conn: sqlite3.Connection, request_uid: str, - assets_and_pools: dict[str, dict[str, PoolModel] | int], + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], allocations: dict[str, AllocInfo], axon_times: list, request_type: REQUEST_TYPE, diff --git a/tests/unit/validator/test_simulator.py b/tests/unit/validator/test_simulator.py deleted file mode 100644 index d540ad6..0000000 --- a/tests/unit/validator/test_simulator.py +++ /dev/null @@ -1,235 +0,0 @@ -import unittest -from sturdy.utils.ethmath import wei_div -from sturdy.validator.simulator import Simulator -from sturdy.constants import * -from sturdy.utils.misc import borrow_rate -import numpy as np -import copy - - -def chk_eq_state(init_state, new_state): - return ( - init_state[0] == new_state[0] # Compare the type of PRNG - and np.array_equal(init_state[1], new_state[1]) # Compare the state of the PRNG - and init_state[2] == new_state[2] # Compare the position in the PRNG's state - and init_state[3] == new_state[3] # Compare the position in the PRNG's buffer - and init_state[4] == new_state[4] # Compare the state of the PRNG's buffer - ) - - -class TestSimulator(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.simulator = Simulator(reversion_speed=0.05) - - def test_init_data(self): - self.simulator.rng_state_container = np.random.RandomState(69) - self.simulator.init_rng = np.random.RandomState(69) - self.simulator.init_data() - self.assertIsNotNone(self.simulator.assets_and_pools) - self.assertIsNotNone(self.simulator.allocations) - self.assertIsNotNone(self.simulator.pool_history) - self.assertEqual(len(self.simulator.pool_history), 1) - - initial_pool_data = self.simulator.pool_history[0] - self.assertEqual(len(initial_pool_data), NUM_POOLS) - - for pool in initial_pool_data.values(): - self.assertTrue(hasattr(pool, "borrow_amount")) - self.assertTrue(hasattr(pool, "reserve_size")) - self.assertTrue(hasattr(pool, "borrow_rate")) - self.assertGreaterEqual(pool.borrow_amount, 0) - self.assertGreaterEqual(pool.reserve_size, pool.borrow_amount) - self.assertGreaterEqual(pool.borrow_rate, 0) - - self.simulator = Simulator( - reversion_speed=0.05, - ) - - # should raise error - self.assertRaises(RuntimeError, self.simulator.init_data) - - def test_update_reserves_with_allocs(self): - self.simulator.rng_state_container = np.random.RandomState(69) - self.simulator.init_rng = np.random.RandomState(69) - self.simulator.init_data() - - init_pools = copy.deepcopy(self.simulator.assets_and_pools["pools"]) - - contract_addresses = [addr for addr in self.simulator.assets_and_pools["pools"]] - - allocations = { - contract_addresses[i]: self.simulator.assets_and_pools["total_assets"] / len(init_pools) - for i in range(len(init_pools)) - } - - self.simulator.update_reserves_with_allocs(allocations) - - for uid, init_pool in init_pools.items(): - # check pools - new_pool = self.simulator.assets_and_pools["pools"][uid] - reserve_should_be = allocations[uid] + init_pool.reserve_size - self.assertEqual(reserve_should_be, new_pool.reserve_size) - - # check init pool_history datapoint - new_pool_hist_init = self.simulator.pool_history[0][uid] - b_rate_should_be = borrow_rate( - wei_div(new_pool_hist_init.borrow_amount, new_pool_hist_init.reserve_size), - new_pool, - ) - self.assertEqual(reserve_should_be, new_pool_hist_init.reserve_size) - self.assertEqual(b_rate_should_be, new_pool_hist_init.borrow_rate) - - # we shouldn't need to list out all the pools we are allocating to - # the ones that are not lists will not be allocated to at all - def test_update_reserves_with_allocs_partial(self): - self.simulator.rng_state_container = np.random.RandomState(69) - self.simulator.init_rng = np.random.RandomState(69) - self.simulator.init_data() - - init_pools = copy.deepcopy(self.simulator.assets_and_pools["pools"]) - total_assets = self.simulator.assets_and_pools["total_assets"] - - contract_addresses = [addr for addr in self.simulator.assets_and_pools["pools"]] - - allocs = {contract_addresses[0]: total_assets / 10} # should be 0.1 if total assets is 1 - - self.simulator.update_reserves_with_allocs(allocs) - - for uid, alloc in allocs.items(): - # for uid, init_pool in init_pools.items(): - # check pools - init_pool = init_pools[uid] - new_pool = self.simulator.assets_and_pools["pools"][uid] - reserve_should_be = alloc + init_pool.reserve_size - self.assertEqual(reserve_should_be, new_pool.reserve_size) - - # check init pool_history datapoint - new_pool_hist_init = self.simulator.pool_history[0][uid] - b_rate_should_be = borrow_rate( - wei_div(new_pool_hist_init.borrow_amount, new_pool_hist_init.reserve_size), - new_pool, - ) - self.assertEqual(reserve_should_be, new_pool_hist_init.reserve_size) - self.assertEqual(b_rate_should_be, new_pool_hist_init.borrow_rate) - - def test_initialization(self): - self.simulator.initialize(timesteps=50) - self.assertIsNotNone(self.simulator.init_rng) - self.assertIsNotNone(self.simulator.rng_state_container) - init_state_container = copy.deepcopy(self.simulator.init_rng) - init_state = init_state_container.get_state() - rng_state = self.simulator.rng_state_container.get_state() - states_equal = chk_eq_state(init_state, rng_state) - self.assertTrue(states_equal) - - # should reinit with fresh rng state - self.simulator.initialize(timesteps=50) - new_state_container = copy.deepcopy(self.simulator.rng_state_container) - - new_state = new_state_container.get_state() - are_states_equal = chk_eq_state(init_state, new_state) - - self.assertFalse(are_states_equal) - - def test_reset(self): - self.simulator.initialize(timesteps=50) - self.simulator.init_data() - init_state_container = copy.deepcopy(self.simulator.init_rng) - init_state = init_state_container.get_state() - init_assets_pools = copy.deepcopy(self.simulator.assets_and_pools) - init_allocs = copy.deepcopy(self.simulator.allocations) - # use the rng - for i in range(10): - self.simulator.rng_state_container.rand() - - after_container = self.simulator.rng_state_container - new_state = after_container.get_state() - - are_states_equal = chk_eq_state(init_state, new_state) - self.assertFalse(are_states_equal) - - self.simulator.reset() - - new_init_state_container = self.simulator.init_rng - new_state_container = self.simulator.rng_state_container - new_init_state = new_init_state_container.get_state() - new_state = new_state_container.get_state() - - are_states_equal = chk_eq_state(init_state, new_init_state) - self.assertTrue(are_states_equal) - - are_states_equal = chk_eq_state(new_state, new_init_state) - self.assertTrue(are_states_equal) - - new_assets_pools = copy.deepcopy(self.simulator.assets_and_pools) - new_allocs = copy.deepcopy(self.simulator.allocations) - - self.assertEqual(init_allocs, new_allocs) - self.assertEqual(init_assets_pools, new_assets_pools) - - self.simulator = Simulator( - reversion_speed=0.05, - ) - - # should raise error - self.assertRaises(RuntimeError, self.simulator.reset) - - def test_sim_run(self): - self.simulator.initialize(timesteps=50) - self.simulator.init_data() - self.simulator.run() - - self.assertEqual(len(self.simulator.pool_history), self.simulator.timesteps) - - # test to see if we're recording the right things - - for t in range(1, self.simulator.timesteps): - pool_data = self.simulator.pool_history[t] - self.assertEqual(len(pool_data), NUM_POOLS) - - for contract_addr, pool in pool_data.items(): - self.assertTrue(hasattr(pool, "borrow_amount")) - self.assertTrue(hasattr(pool, "reserve_size")) - self.assertTrue(hasattr(pool, "borrow_rate")) - self.assertGreaterEqual(pool.borrow_amount, 0) - self.assertGreaterEqual(pool.reserve_size, pool.borrow_amount) - self.assertGreaterEqual(pool.borrow_rate, 0) - - for contract_addr, _ in self.simulator.assets_and_pools["pools"].items(): - borrow_amounts = [ - self.simulator.pool_history[T][contract_addr].borrow_amount for T in range(1, self.simulator.timesteps) - ] - borrow_rates = [ - self.simulator.pool_history[T][contract_addr].borrow_rate for T in range(1, self.simulator.timesteps) - ] - - self.assertTrue(borrow_amounts.count(borrow_amounts[0]) < len(borrow_amounts)) - self.assertTrue(borrow_rates.count(borrow_rates[0]) < len(borrow_rates)) - - # check if simulation runs the same across "reset()s" - # first run - self.simulator.initialize(timesteps=50) - self.simulator.init_data() - self.simulator.run() - pool_history0 = copy.deepcopy(self.simulator.pool_history) - # second run - after reset - self.simulator.reset() - self.simulator.init_data() - self.simulator.run() - pool_history1 = self.simulator.pool_history - self.assertEqual(pool_history0, pool_history1) - - self.simulator = Simulator( - reversion_speed=0.05, - ) - - # should raise error - self.assertRaises(RuntimeError, self.simulator.run) - - # pp.pprint(f"assets and pools: \n {self.validator.assets_and_pools}") - # pp.pprint(f"pool history: \n {self.validator.pool_history}") - - -if __name__ == "__main__": - unittest.main() From 73aa880dc250991e181bb869a45b5bf775aca941 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Fri, 1 Nov 2024 11:22:58 +0000 Subject: [PATCH 04/51] feat: almost done with scoring --- neurons/validator.py | 2 +- sturdy/validator/forward.py | 23 ++++++++++--------- sturdy/validator/reward.py | 45 +++++++++++++++++++++++++++++++------ sturdy/validator/sql.py | 17 +++++++------- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/neurons/validator.py b/neurons/validator.py index 505c833..83ae285 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -261,7 +261,7 @@ async def get_allocations( to_ts: int | None = None, ) -> list[dict]: with sql.get_db_connection() as conn: - allocations = sql.get_filtered_allocations(conn, request_uid, miner_uid, from_ts, to_ts) + allocations = sql.get_miner_responses(conn, request_uid, miner_uid, from_ts, to_ts) if not allocations: raise HTTPException(status_code=404, detail="No allocations found") return allocations diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 771289f..4a71be6 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -26,7 +26,7 @@ from sturdy.constants import QUERY_TIMEOUT, SCORING_PERIOD from sturdy.pools import generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo -from sturdy.validator.reward import filter_allocations +from sturdy.validator.reward import filter_allocations, get_rewards from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations @@ -123,11 +123,20 @@ async def query_and_score_miners( # score previously suggested miner allocations based on how well they are performing now # get all the request ids for the pools we should be scoring from the db - active_allocs = [] + active_alloc_rows = [] with get_db_connection() as conn: - active_allocs = get_active_allocs(conn) + active_alloc_rows = get_active_allocs(conn) - bt.logging.debug(f"Active allocs: {active_allocs}") + bt.logging.debug(f"Active allocs: {active_alloc_rows}") + + for active_alloc in active_alloc_rows: + # calculate rewards for previous active allocations + miner_uids, rewards = get_rewards(self, active_alloc) + bt.logging.debug(f"miner rewards: {rewards}") + + # update the moving average scores of the miners + int_miner_uids = [int(uid) for uid in miner_uids] + # self.update_scores(rewards, int_miner_uids) # before logging latest allocations # filter them @@ -135,16 +144,10 @@ async def query_and_score_miners( self, query=self.step, uids=active_uids, - active_allocations=active_allocs, responses=responses, assets_and_pools=assets_and_pools, ) - # calculate rewards for previous active allocations - - # update the moving average scores of the miners - # int_active_uids = [int(uid) for uid in active_uids] - # self.update_scores(rewards, int_active_uids) # TODO: sort the miners' by their current scores and return their respective allocations diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index e6601fd..6ec47b5 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -16,7 +16,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -import copy +import json from typing import Any, cast import bittensor as bt @@ -27,8 +27,9 @@ from sturdy.constants import QUERY_TIMEOUT, SIMILARITY_THRESHOLD from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, check_allocations -from sturdy.protocol import REQUEST_TYPES, AllocationsDict, AllocInfo +from sturdy.protocol import AllocationsDict, AllocInfo from sturdy.utils.ethmath import wei_div, wei_mul +from sturdy.validator.sql import get_db_connection, get_miner_responses, get_request_info def get_response_times(uids: list[str], responses, timeout: float) -> dict[str, float]: @@ -265,8 +266,6 @@ def _get_rewards( - adjusted_rewards: The reward values for the miners. """ - # raw_apys = torch.Tensor([apys_and_allocations[uid]["apy"] for uid in uids]) - rewards_apy = dynamic_normalize_zscore(apys_and_allocations).to(self.device) return adjust_rewards_for_plagiarism(self, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times) @@ -325,7 +324,6 @@ def filter_allocations( self, query: int, # noqa: ARG001 uids: list[str], - active_allocations: list, # row of allocations from database responses: list, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], ) -> dict[str, AllocInfo]: @@ -345,7 +343,6 @@ def filter_allocations( axon_times = get_response_times(uids=uids, responses=responses, timeout=QUERY_TIMEOUT) for response_idx, response in enumerate(responses): - allocations = response.allocations # validator miner allocations before running simulation @@ -381,4 +378,38 @@ def filter_allocations( # Get all the reward results by iteratively calling your reward() function. return axon_times, curr_filtered_allocs -# def get_rewards() \ No newline at end of file + +def get_rewards(self, active_allocation) -> tuple[list, dict]: + # a dictionary, miner uids -> apy and allocations + apys_and_allocations = {} + miner_uids = [] + axon_times = [] + + # TODO: rename this here and in the database schema? + request_uid = active_allocation["request_uid"] + assets_and_pools = None + miners = None + + with get_db_connection() as conn: + # get assets and pools that are used to benchmark miner + assets_and_pools = get_request_info(conn, request_uid=request_uid) + + # obtain the miner responses for each request + miners = get_miner_responses(conn, request_uid=request_uid) + bt.logging.debug(f"filtered allocations: {miners}") + + # TODO: this probably needs more work + # TODO: would it be better here to use metrics i.e. liquidityIndex for aave pools? + # calculate the "adjusted" yields of the allocations + for miner in miners: + allocations = json.loads(miner["allocation"])["allocations"] + miner_uid = miner["miner_uid"] + miner_apy = calculate_apy(allocations, assets_and_pools) + miner_axon_time = miner["axon_time"] + + miner_uids.append(miner_uid) + axon_times.append(miner_axon_time) + apys_and_allocations[miner_uid] = {"apy": miner_apy, "allocations": allocations} + + # get rewards given the apys and allocations(s) with _get_rewards (???) + return (miner_uids, _get_rewards(self, apys_and_allocations, assets_and_pools, miner_uids, axon_times)) diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index deba3c4..46fa84b 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -199,6 +199,7 @@ def log_allocations( conn.commit() +# TODO: rename function and database table? def get_active_allocs( conn: sqlite3.Connection, ) -> list: @@ -221,12 +222,12 @@ def get_active_allocs( return [dict(row) for row in rows] -def get_filtered_allocations( +def get_miner_responses( conn: sqlite3.Connection, - request_uid: str | None, - miner_uid: str | None, - from_ts: int | None, - to_ts: int | None, + request_uid: str | None = None, + miner_uid: str | None = None, + from_ts: int | None = None, + to_ts: int | None = None, ) -> list[dict]: query = f""" SELECT * FROM {ALLOCATIONS_TABLE} @@ -257,9 +258,9 @@ def get_filtered_allocations( def get_request_info( conn: sqlite3.Connection, - request_uid: str | None, - from_ts: int | None, - to_ts: int | None, + request_uid: str | None = None, + from_ts: int | None = None, + to_ts: int | None = None, ) -> list[dict]: query = f""" SELECT * FROM {ALLOCATION_REQUESTS_TABLE} From 4c36e4e6c012ad5f532f7fc70bfb2660371ca382 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 3 Nov 2024 12:56:12 +0000 Subject: [PATCH 05/51] feat: scoring system draft --- sturdy/algo.py | 9 ++----- sturdy/validator/forward.py | 6 ++++- sturdy/validator/reward.py | 47 +++++++++++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/sturdy/algo.py b/sturdy/algo.py index 456c1e7..fcc75e1 100644 --- a/sturdy/algo.py +++ b/sturdy/algo.py @@ -30,7 +30,6 @@ def naive_algorithm(self: BaseMinerNeuron, synapse: AllocateAssets) -> dict: contract_address=pool.contract_address, ) - total_assets_available = int(THRESHOLD * synapse.assets_and_pools["total_assets"]) pools = cast(dict, synapse.assets_and_pools["pools"]) @@ -40,14 +39,10 @@ def naive_algorithm(self: BaseMinerNeuron, synapse: AllocateAssets) -> dict: # sync pool parameters by calling smart contracts on chain for pool in pools.values(): match pool.pool_type: - case POOL_TYPES.AAVE: - pool.sync(synapse.user_address, self.w3) - case POOL_TYPES.STURDY_SILO: + case P if P in (POOL_TYPES.AAVE, POOL_TYPES.STURDY_SILO): pool.sync(synapse.user_address, self.w3) - case T if T in (POOL_TYPES.DAI_SAVINGS, POOL_TYPES.COMPOUND_V3): - pool.sync(self.w3) case _: - pass + pool.sync(self.w3) # check the amounts that have been borrowed from the pools - and account for them minimums = {} diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 4a71be6..2f2ae5e 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -134,9 +134,13 @@ async def query_and_score_miners( miner_uids, rewards = get_rewards(self, active_alloc) bt.logging.debug(f"miner rewards: {rewards}") + # TODO: there may be a better way to go about this + if len(miner_uids) < 1: + break + # update the moving average scores of the miners int_miner_uids = [int(uid) for uid in miner_uids] - # self.update_scores(rewards, int_miner_uids) + self.update_scores(rewards, int_miner_uids) # before logging latest allocations # filter them diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 6ec47b5..b7c9a55 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -24,9 +24,10 @@ import numpy as np import numpy.typing as npt import torch +from web3.constants import ADDRESS_ZERO from sturdy.constants import QUERY_TIMEOUT, SIMILARITY_THRESHOLD -from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, check_allocations +from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, PoolFactory, check_allocations from sturdy.protocol import AllocationsDict, AllocInfo from sturdy.utils.ethmath import wei_div, wei_mul from sturdy.validator.sql import get_db_connection, get_miner_responses, get_request_info @@ -87,6 +88,11 @@ def dynamic_normalize_zscore( apys_and_allocations: AllocationsDict, z_threshold: float = 1.0, q: float = 0.75, epsilon: float = 1e-8 ) -> torch.Tensor: raw_apys = {uid: apys_and_allocations[uid]["apy"] for uid in apys_and_allocations} + + # TODO: is there a better way to go about this? + if len(raw_apys) <= 1: + return torch.zeros(len(raw_apys)) + sorted_apys_uid = dict(sorted(raw_apys.items(), key=lambda item: item[1])) apys = torch.tensor(list(raw_apys.values())) sorted_apys = torch.tensor(list(sorted_apys_uid.values())) @@ -383,7 +389,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: # a dictionary, miner uids -> apy and allocations apys_and_allocations = {} miner_uids = [] - axon_times = [] + axon_times = {} # TODO: rename this here and in the database schema? request_uid = active_allocation["request_uid"] @@ -392,12 +398,41 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: with get_db_connection() as conn: # get assets and pools that are used to benchmark miner - assets_and_pools = get_request_info(conn, request_uid=request_uid) + # we get the first row entry - we assume that it is the only response from the database + try: + assets_and_pools = json.loads(get_request_info(conn, request_uid=request_uid)[0]["assets_and_pools"]) + except Exception: + return ([], {}) # obtain the miner responses for each request miners = get_miner_responses(conn, request_uid=request_uid) bt.logging.debug(f"filtered allocations: {miners}") + # TODO: see if we can factor this into its own subroutine + # if so, do the same with the same one in validator.py + + pools = assets_and_pools["pools"] + new_pools = {} + for uid, pool in pools.items(): + new_pool = PoolFactory.create_pool( + pool_type=pool["pool_type"], + web3_provider=self.w3, # type: ignore[] + user_address=(pool["user_address"]), # TODO: is there a cleaner way to do this? + contract_address=pool["contract_address"], + ) + + # sync pool + # TODO: find these elsewhere and probably factor these into their own method? + match new_pool.pool_type: + case P if P in (POOL_TYPES.AAVE, POOL_TYPES.STURDY_SILO): + new_pool.sync(new_pool.user_address, self.w3) + case _: + new_pool.sync(self.w3) + + new_pools[uid] = new_pool + + assets_and_pools["pools"] = new_pools + # TODO: this probably needs more work # TODO: would it be better here to use metrics i.e. liquidityIndex for aave pools? # calculate the "adjusted" yields of the allocations @@ -408,8 +443,12 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: miner_axon_time = miner["axon_time"] miner_uids.append(miner_uid) - axon_times.append(miner_axon_time) + axon_times[miner_uid] = miner_axon_time apys_and_allocations[miner_uid] = {"apy": miner_apy, "allocations": allocations} + # TODO: there may be a better way to go about this + if len(miner_uids) < 1: + return ([], {}) + # get rewards given the apys and allocations(s) with _get_rewards (???) return (miner_uids, _get_rewards(self, apys_and_allocations, assets_and_pools, miner_uids, axon_times)) From b55c2707f8452a6550690040c58be35e76446a5d Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 3 Nov 2024 12:56:40 +0000 Subject: [PATCH 06/51] chore: ruff format --- sturdy/base/miner.py | 13 +++---------- sturdy/validator/forward.py | 3 +-- sturdy/validator/sql.py | 9 +-------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/sturdy/base/miner.py b/sturdy/base/miner.py index b09c7ab..9f6877c 100644 --- a/sturdy/base/miner.py +++ b/sturdy/base/miner.py @@ -54,17 +54,13 @@ def __init__(self, config=None): w3_provider_url = os.environ.get("WEB3_PROVIDER_URL") if w3_provider_url is None: - raise ValueError( - "You must provide a valid web3 provider url in order to handle organic requests!" - ) + raise ValueError("You must provide a valid web3 provider url in order to handle organic requests!") self.w3 = Web3(Web3.HTTPProvider(w3_provider_url)) # Warn if allowing incoming requests from anyone. if not self.config.blacklist.force_validator_permit: - bt.logging.warning( - "You are allowing non-validators to send requests to your miner. This is a security risk." - ) + bt.logging.warning("You are allowing non-validators to send requests to your miner. This is a security risk.") if self.config.blacklist.allow_non_registered: bt.logging.warning( "You are allowing non-registered entities to send requests to your miner. This is a security risk." @@ -132,10 +128,7 @@ def run(self): # This loop maintains the miner's operations until intentionally stopped. try: while not self.should_exit: - while ( - self.block - self.metagraph.last_update[self.uid] - < self.config.neuron.epoch_length - ): + while self.block - self.metagraph.last_update[self.uid] < self.config.neuron.epoch_length: # Wait before checking again. time.sleep(1) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 2f2ae5e..d5daf67 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -60,7 +60,7 @@ async def forward(self) -> Any: allocations, axon_times, REQUEST_TYPES.SYNTHETIC, - SCORING_PERIOD + SCORING_PERIOD, ) @@ -152,7 +152,6 @@ async def query_and_score_miners( assets_and_pools=assets_and_pools, ) - # TODO: sort the miners' by their current scores and return their respective allocations return axon_times, filtered_allocs diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 46fa84b..47cea96 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -185,13 +185,7 @@ def log_allocations( to_insert = [] for miner_uid, miner_allocation in allocations.items(): - row = ( - request_uid, - miner_uid, - to_json_string(miner_allocation), - datetime_now, - axon_times[miner_uid] - ) + row = (request_uid, miner_uid, to_json_string(miner_allocation), datetime_now, axon_times[miner_uid]) to_insert.append(row) conn.executemany(f"INSERT INTO {ALLOCATIONS_TABLE} VALUES (?, ?, json(?), ?, ?)", to_insert) @@ -203,7 +197,6 @@ def log_allocations( def get_active_allocs( conn: sqlite3.Connection, ) -> list: - # TODO: change the logic of handling "active allocations" # for now we simply get ones which are still in their "challenge" # period, and consider them to determine the score of miners From d81dfa456870aa5f17b8e95b93863dc20da71e94 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Mon, 4 Nov 2024 04:00:00 +0000 Subject: [PATCH 07/51] feat: draft done - works with sturdy pools. onward --- sturdy/validator/forward.py | 8 ++++-- sturdy/validator/reward.py | 27 +++------------------ tests/unit/validator/test_reward_helpers.py | 16 ++++++------ 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index d5daf67..865ecb8 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -24,7 +24,8 @@ from web3.constants import ADDRESS_ZERO from sturdy.constants import QUERY_TIMEOUT, SCORING_PERIOD -from sturdy.pools import generate_challenge_data +from sturdy.pool_registry.pool_registry import POOL_REGISTRY +from sturdy.pools import assets_pools_for_challenge_data, generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations @@ -42,7 +43,10 @@ async def forward(self) -> Any: """ # initialize pools and assets - challenge_data = generate_challenge_data(web3_provider=self.w3) + # challenge_data = generate_challenge_data(web3_provider=self.w3) + # TODO: only sturdy pools for now + selected_entry = POOL_REGISTRY["Sturdy Crvusd Aggregator"] + challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) request_uuid = str(uuid.uuid4()).replace("-", "") axon_times, allocations = await query_and_score_miners( diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index b7c9a55..69e4252 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -84,7 +84,7 @@ def format_allocations( return {contract_addr: allocs[contract_addr] for contract_addr in sorted(allocs.keys())} -def dynamic_normalize_zscore( +def normalize_squared( apys_and_allocations: AllocationsDict, z_threshold: float = 1.0, q: float = 0.75, epsilon: float = 1e-8 ) -> torch.Tensor: raw_apys = {uid: apys_and_allocations[uid]["apy"] for uid in apys_and_allocations} @@ -93,29 +93,9 @@ def dynamic_normalize_zscore( if len(raw_apys) <= 1: return torch.zeros(len(raw_apys)) - sorted_apys_uid = dict(sorted(raw_apys.items(), key=lambda item: item[1])) apys = torch.tensor(list(raw_apys.values())) - sorted_apys = torch.tensor(list(sorted_apys_uid.values())) - quantile = np.percentile(sorted_apys.numpy(), q) - apy_grad = [abs(sorted_apys[i] - sorted_apys[i - 1]) for i in range(1, len(sorted_apys))] - mean_grad = np.mean(apy_grad) - std_grad = np.std(apy_grad) - apy_grad.insert(0, float("nan")) - apy_grad = torch.tensor(apy_grad) - - # Calculate z-scores - z_scores = (apy_grad - mean_grad) / std_grad - - # Set a lower bound based on z-score threshold if the lower quartile range is larger than the rest - filtered = sorted_apys[(z_scores > z_threshold) & (sorted_apys < quantile)] - lower_bound = filtered.min() if len(filtered) > 0 else sorted_apys.min() - - # No upper bound, only clip the lower bound - clipped_data = torch.clip(apys, lower_bound) - - dynamic_normed = (clipped_data - clipped_data.min()) / (clipped_data.max() - clipped_data.min() + epsilon) - squared = torch.pow(dynamic_normed, 2) + squared = torch.pow(apys, 2) return (squared - squared.min()) / (squared.max() - squared.min() + epsilon) @@ -252,6 +232,7 @@ def adjust_rewards_for_plagiarism( # Step 2: Apply penalties considering axon times penalties = calculate_penalties(similarity_matrix, axon_times, similarity_threshold) self.similarity_penalties = penalties + bt.logging.debug(f"sim penalities: {self.similarity_penalties}") # Step 3: Calculate final rewards with adjusted penalties return calculate_rewards_with_adjusted_penalties(uids, rewards_apy, penalties) @@ -272,7 +253,7 @@ def _get_rewards( - adjusted_rewards: The reward values for the miners. """ - rewards_apy = dynamic_normalize_zscore(apys_and_allocations).to(self.device) + rewards_apy = normalize_squared(apys_and_allocations).to(self.device) return adjust_rewards_for_plagiarism(self, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 351852c..cfe4807 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -14,7 +14,7 @@ adjust_rewards_for_plagiarism, calculate_penalties, calculate_rewards_with_adjusted_penalties, - dynamic_normalize_zscore, + normalize_squared, format_allocations, get_distance, get_similarity_matrix, @@ -85,7 +85,7 @@ class TestDynamicNormalizeZScore(unittest.TestCase): def test_basic_normalization(self) -> None: # Test a simple AllocationsDict with large values apys_and_allocations = {"1": {"apy": 1e16}, "2": {"apy": 2e16}, "3": {"apy": 3e16}, "4": {"apy": 4e16}} - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # Check if output is normalized between 0 and 1 self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -100,7 +100,7 @@ def test_with_low_outliers(self) -> None: "4": {"apy": 5e16}, "5": {"apy": 1e17}, } - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # Check that outliers don't affect the overall normalization self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -115,7 +115,7 @@ def test_with_high_outliers(self) -> None: "4": {"apy": 1e17}, "5": {"apy": 2e17}, } - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # Check that the function correctly handles high outliers self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -124,7 +124,7 @@ def test_with_high_outliers(self) -> None: def test_uniform_values(self) -> None: # Test where all values are the same apys_and_allocations = {"1": {"apy": 1e16}, "2": {"apy": 1e16}, "3": {"apy": 1e16}, "4": {"apy": 1e16}} - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # If all values are the same, the output should also be uniform (or handle gracefully) self.assertTrue( @@ -142,7 +142,7 @@ def test_low_variance(self) -> None: "4": {"apy": 1.03e16}, "5": {"apy": 1.04e16}, } - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # Check if normalization happens correctly self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -151,7 +151,7 @@ def test_low_variance(self) -> None: def test_high_variance(self) -> None: # Test with high variance data apys_and_allocations = {"1": {"apy": 1e16}, "2": {"apy": 1e17}, "3": {"apy": 5e17}, "4": {"apy": 1e18}} - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # Ensure that the normalization works even with high variance self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -169,7 +169,7 @@ def test_quantile_logic(self) -> None: "7": {"apy": 3e17}, "8": {"apy": 4e17}, } - normalized = dynamic_normalize_zscore(apys_and_allocations) + normalized = normalize_squared(apys_and_allocations) # Ensure that quantile-based clipping works as expected self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) From f2b56f7186cf792c442b1adbe3f204bc35ac3eb7 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 5 Nov 2024 08:23:13 +0000 Subject: [PATCH 08/51] feat: working on tests --- db/migrations/20241030231410_alloc_table.sql | 2 + neurons/validator.py | 9 + sturdy/abi/SturdySiloStrategy.json | 13 + sturdy/algo.py | 6 +- sturdy/base/neuron.py | 26 +- sturdy/base/validator.py | 10 +- sturdy/constants.py | 2 +- sturdy/pools.py | 28 +- sturdy/validator/forward.py | 14 +- sturdy/validator/reward.py | 36 +-- sturdy/validator/sql.py | 5 +- .../validator/test_integration_validator.py | 262 +++++++++++++++--- tests/unit/validator/test_pool_generator.py | 7 +- tests/unit/validator/test_pool_models.py | 24 +- tests/unit/validator/test_reward_helpers.py | 122 +------- tests/unit/validator/test_validator.py | 237 ---------------- 16 files changed, 342 insertions(+), 461 deletions(-) delete mode 100644 tests/unit/validator/test_validator.py diff --git a/db/migrations/20241030231410_alloc_table.sql b/db/migrations/20241030231410_alloc_table.sql index 3de21e6..57cf3a6 100644 --- a/db/migrations/20241030231410_alloc_table.sql +++ b/db/migrations/20241030231410_alloc_table.sql @@ -26,6 +26,8 @@ CREATE TABLE IF NOT EXISTS allocations ( -- This alter statement adds a new column to the allocations table if it exists ALTER TABLE allocation_requests ADD COLUMN request_type TEXT NOT NULL DEFAULT 'SYNTHETIC'; +ALTER TABLE allocation_requests +ADD COLUMN metadata TEXT; ALTER TABLE allocations ADD COLUMN axon_time FLOAT NOT NULL DEFAULT 99999.0; -- large number for now diff --git a/neurons/validator.py b/neurons/validator.py index 83ae285..322d497 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -238,12 +238,21 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None to_ret = dict(list(result.items())[: body.num_allocs]) ret = AllocateAssetsResponse(allocations=to_ret, request_uuid=request_uuid) to_log = AllocateAssetsResponse(allocations=to_ret, request_uuid=request_uuid) + + metadata = {} + pools = synapse.assets_and_pools["pools"] + + for contract_addr, pool in pools.items(): + pool.sync(core_validator.w3) + metadata[contract_addr] = pool._price_per_share + with sql.get_db_connection() as conn: # TODO: make challenge period variable and based on user input sql.log_allocations( conn, to_log.request_uuid, synapse.assets_and_pools, + metadata, to_log.allocations, axon_times, REQUEST_TYPES.ORGANIC, diff --git a/sturdy/abi/SturdySiloStrategy.json b/sturdy/abi/SturdySiloStrategy.json index 7ea3ed1..281c6d1 100644 --- a/sturdy/abi/SturdySiloStrategy.json +++ b/sturdy/abi/SturdySiloStrategy.json @@ -30,6 +30,19 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "name": "pricePerShare", + "type": "function", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "stateMutability": "nonpayable", "type": "fallback" diff --git a/sturdy/algo.py b/sturdy/algo.py index fcc75e1..17cfab9 100644 --- a/sturdy/algo.py +++ b/sturdy/algo.py @@ -38,11 +38,7 @@ def naive_algorithm(self: BaseMinerNeuron, synapse: AllocateAssets) -> dict: # sync pool parameters by calling smart contracts on chain for pool in pools.values(): - match pool.pool_type: - case P if P in (POOL_TYPES.AAVE, POOL_TYPES.STURDY_SILO): - pool.sync(synapse.user_address, self.w3) - case _: - pool.sync(self.w3) + pool.sync(self.w3) # check the amounts that have been borrowed from the pools - and account for them minimums = {} diff --git a/sturdy/base/neuron.py b/sturdy/base/neuron.py index 5f0b9e5..559a8df 100644 --- a/sturdy/base/neuron.py +++ b/sturdy/base/neuron.py @@ -123,21 +123,25 @@ def sync(self): Wrapper for synchronizing the state of the network for the given miner or validator. """ # Ensure miner or validator hotkey is still registered on the network. - self.check_registered() - if self.should_sync_metagraph(): - try: + try: + self.check_registered() + except Exception: + bt.logging.error("Could not check registration status! Skipping...") + + try: + if self.should_sync_metagraph(): self.resync_metagraph() - except Exception as e: - bt.logging.error("There was an issue with trying to sync with the metagraph! See Error:") - bt.logging.error(e) + except Exception as e: + bt.logging.error("There was an issue with trying to sync with the metagraph! See Error:") + bt.logging.error(e) - if self.should_set_weights(): - try: + try: + if self.should_set_weights(): self.set_weights() - except Exception as e: - bt.logging.error("Failed to set weights! See Error:") - bt.logging.error(e) + except Exception as e: + bt.logging.error("Failed to set weights! See Error:") + bt.logging.error(e) # Always save state. self.save_state() diff --git a/sturdy/base/validator.py b/sturdy/base/validator.py index 0fb4724..0152d8b 100644 --- a/sturdy/base/validator.py +++ b/sturdy/base/validator.py @@ -61,12 +61,12 @@ def __init__(self, config=None) -> None: # Save a copy of the hotkeys to local memory. self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) - if self.config.organic: - w3_provider_url = os.environ.get("WEB3_PROVIDER_URL") - if w3_provider_url is None: - raise ValueError("You must provide a valid web3 provider url as an organic validator!") + # set web3 provider url + w3_provider_url = os.environ.get("WEB3_PROVIDER_URL") + if w3_provider_url is None: + raise ValueError("You must provide a valid web3 provider url as an organic validator!") - self.w3 = Web3(Web3.HTTPProvider(w3_provider_url)) + self.w3 = Web3(Web3.HTTPProvider(w3_provider_url)) # Dendrite lets us send messages to other nodes (axons) in the network. if self.config.mock: diff --git a/sturdy/constants.py b/sturdy/constants.py index df861e8..68c622f 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -36,7 +36,7 @@ # TODO: make scoring period variable and random? -SCORING_PERIOD = 120 +SCORING_PERIOD = 300 # The following constants are for different pool models # Aave diff --git a/sturdy/pools.py b/sturdy/pools.py index 3724010..f8b6d72 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -79,8 +79,8 @@ def get_minimum_allocation(pool: "ChainBasedPoolModel") -> int: return pool._curr_deposit - pool._max_withdraw case POOL_TYPES.DAI_SAVINGS: pass # TODO: is there a more appropriate way to go about this? - case _: # we assume it is a synthetic pool - return pool.borrow_amount + case _: # not a valid pool type + return 1 return 0 if borrow_amount <= assets_available else assets_available if our_supply >= assets_available else 0 @@ -303,7 +303,7 @@ def pool_init(self, web3_provider: Web3) -> None: bt.logging.error("Failed to load contract!") bt.logging.error(err) # type: ignore[] - def sync(self, user_addr: str, web3_provider: Web3) -> None: + def sync(self, web3_provider: Web3) -> None: """Syncs with chain""" if not self._initted: self.pool_init(web3_provider) @@ -376,7 +376,7 @@ def sync(self, user_addr: str, web3_provider: Web3) -> None: self._reserveFactor = getReserveFactor(reserveConfiguration) self._decimals = retry_with_backoff(self._underlying_asset_contract.functions.decimals().call) self._collateral_amount = retry_with_backoff( - self._atoken_contract.functions.balanceOf(Web3.to_checksum_address(user_addr)).call + self._atoken_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call ) except Exception as err: @@ -437,8 +437,9 @@ class VariableInterestSturdySiloStrategy(ChainBasedPoolModel): _decimals: int = PrivateAttr() _asset: Contract = PrivateAttr() - _user_asset_balance: Contract = PrivateAttr() - _user_total_assets: Contract = PrivateAttr() + _user_asset_balance: int = PrivateAttr() + _user_total_assets: int = PrivateAttr() + _price_per_share: Contract = PrivateAttr() def __hash__(self) -> int: return hash((self._silo_strategy_contract.address, self._pair_contract)) @@ -452,7 +453,7 @@ def __eq__(self, other) -> bool: other._pair_contract.address, ) - def pool_init(self, user_addr: str, web3_provider: Web3) -> None: # noqa: ARG002 + def pool_init(self, web3_provider: Web3) -> None: # noqa: ARG002 try: assert web3_provider.is_connected() except Exception as err: @@ -501,10 +502,10 @@ def pool_init(self, user_addr: str, web3_provider: Web3) -> None: # noqa: ARG00 except Exception as e: bt.logging.error(e) # type: ignore[] - def sync(self, user_addr: str, web3_provider: Web3) -> None: + def sync(self, web3_provider: Web3) -> None: """Syncs with chain""" if not self._initted: - self.pool_init(user_addr, web3_provider) + self.pool_init(web3_provider) user_shares = retry_with_backoff(self._pair_contract.functions.balanceOf(self.contract_address).call) self._curr_deposit_amount = retry_with_backoff(self._pair_contract.functions.convertToAssets(user_shares).call) @@ -523,6 +524,9 @@ def sync(self, user_addr: str, web3_provider: Web3) -> None: self._user_asset_balance = retry_with_backoff(self._asset.functions.balanceOf(self.user_address).call) + # get current price per share + self._price_per_share = retry_with_backoff(self._silo_strategy_contract.functions.pricePerShare().call) + # last 256 unique calls to this will be cached for the next 60 seconds @ttl_cache(maxsize=256, ttl=60) def supply_rate(self, amount: int) -> int: @@ -931,18 +935,18 @@ def assets_pools_for_challenge_data( first_pool = pool_list[0] total_assets = 0 - match pool.pool_type: + match first_pool.pool_type: case POOL_TYPES.STURDY_SILO: - first_pool.sync(user_address, web3_provider) + first_pool.sync(web3_provider) total_assets = first_pool._user_asset_balance case _: pass for pool in pools.values(): + pool.sync(web3_provider) total_asset = 0 match pool.pool_type: case POOL_TYPES.STURDY_SILO: - pool.sync(user_address, web3_provider) total_asset += pool._curr_deposit_amount case _: pass diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 865ecb8..1dbe8c9 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -56,11 +56,20 @@ async def forward(self) -> Any: user_address=challenge_data["user_address"], ) + assets_and_pools = challenge_data["assets_and_pools"] + pools = assets_and_pools["pools"] + metadata = {} + + for contract_addr, pool in pools.items(): + pool.sync(self.w3) + metadata[contract_addr] = pool._price_per_share + with get_db_connection() as conn: log_allocations( conn, request_uuid, - challenge_data["assets_and_pools"], + assets_and_pools, + metadata, allocations, axon_times, REQUEST_TYPES.SYNTHETIC, @@ -95,7 +104,7 @@ async def query_multiple_miners( async def query_and_score_miners( self, - assets_and_pools: Any = None, + assets_and_pools: Any, request_type: REQUEST_TYPES = REQUEST_TYPES.SYNTHETIC, user_address: str = ADDRESS_ZERO, ) -> tuple[list, dict[str, AllocInfo]]: @@ -137,6 +146,7 @@ async def query_and_score_miners( # calculate rewards for previous active allocations miner_uids, rewards = get_rewards(self, active_alloc) bt.logging.debug(f"miner rewards: {rewards}") + bt.logging.debug(f"sim penalities: {self.similarity_penalties}") # TODO: there may be a better way to go about this if len(miner_uids) < 1: diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 69e4252..e628ba8 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -232,7 +232,6 @@ def adjust_rewards_for_plagiarism( # Step 2: Apply penalties considering axon times penalties = calculate_penalties(similarity_matrix, axon_times, similarity_threshold) self.similarity_penalties = penalties - bt.logging.debug(f"sim penalities: {self.similarity_penalties}") # Step 3: Calculate final rewards with adjusted penalties return calculate_rewards_with_adjusted_penalties(uids, rewards_apy, penalties) @@ -261,6 +260,7 @@ def _get_rewards( def calculate_apy( allocations: AllocationsDict, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], + extra_metadata: dict ) -> int: """ Calculates immediate projected yields given intial assets and pools, pool history, and number of timesteps @@ -269,17 +269,20 @@ def calculate_apy( # calculate projected yield initial_balance = cast(int, assets_and_pools["total_assets"]) pools = cast(dict[str, ChainBasedPoolModel], assets_and_pools["pools"]) - pct_yield = 0 - for uid, pool in pools.items(): - allocation = allocations[uid] + total_yield = 0 + + for contract_addr, pool in pools.items(): + allocation = allocations[contract_addr] match pool.pool_type: - case POOL_TYPES.DAI_SAVINGS: - pool_yield = wei_mul(allocation, pool.supply_rate()) + case POOL_TYPES.STURDY_SILO: + last_share_price = extra_metadata[contract_addr] + curr_share_price = pool._price_per_share + pct_delta = float(last_share_price - curr_share_price) / float(last_share_price) + total_yield += int(allocation * pct_delta) case _: - pool_yield = wei_mul(allocation, pool.supply_rate(amount=allocation)) - pct_yield += pool_yield + total_yield += 0 - return wei_div(pct_yield, initial_balance) + return wei_div(total_yield, initial_balance) def calculate_aggregate_apy( @@ -374,6 +377,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: # TODO: rename this here and in the database schema? request_uid = active_allocation["request_uid"] + request_info = {} assets_and_pools = None miners = None @@ -381,7 +385,8 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: # get assets and pools that are used to benchmark miner # we get the first row entry - we assume that it is the only response from the database try: - assets_and_pools = json.loads(get_request_info(conn, request_uid=request_uid)[0]["assets_and_pools"]) + request_info = get_request_info(conn, request_uid=request_uid)[0] + assets_and_pools = json.loads(request_info["assets_and_pools"]) except Exception: return ([], {}) @@ -403,13 +408,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: ) # sync pool - # TODO: find these elsewhere and probably factor these into their own method? - match new_pool.pool_type: - case P if P in (POOL_TYPES.AAVE, POOL_TYPES.STURDY_SILO): - new_pool.sync(new_pool.user_address, self.w3) - case _: - new_pool.sync(self.w3) - + new_pool.sync(self.w3) new_pools[uid] = new_pool assets_and_pools["pools"] = new_pools @@ -419,8 +418,9 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: # calculate the "adjusted" yields of the allocations for miner in miners: allocations = json.loads(miner["allocation"])["allocations"] + extra_metadata = json.loads(request_info["metadata"]) miner_uid = miner["miner_uid"] - miner_apy = calculate_apy(allocations, assets_and_pools) + miner_apy = calculate_apy(allocations, assets_and_pools, extra_metadata) miner_axon_time = miner["axon_time"] miner_uids.append(miner_uid) diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 47cea96..b3f8987 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -154,6 +154,7 @@ def log_allocations( conn: sqlite3.Connection, request_uid: str, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], + extra_metadata: dict, allocations: dict[str, AllocInfo], axon_times: list, request_type: REQUEST_TYPE, @@ -164,12 +165,14 @@ def log_allocations( scoring_period_end = datetime.fromtimestamp(challenge_end) # noqa: DTZ006 datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 conn.execute( - f"INSERT INTO {ALLOCATION_REQUESTS_TABLE} VALUES (?, json(?), ?, ?)", + f"INSERT INTO {ALLOCATION_REQUESTS_TABLE} VALUES (?, json(?), ?, ?, ?)", ( request_uid, json.dumps(jsonable_encoder(assets_and_pools)), datetime_now, request_type, + # TODO: use jsonable_encoder? + json.dumps(extra_metadata) ), ) diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index 4381422..65b18bb 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -1,78 +1,260 @@ import copy +import os import unittest from unittest import IsolatedAsyncioTestCase import numpy as np +import torch +from dotenv import load_dotenv from neurons.validator import Validator +from sturdy.constants import QUERY_TIMEOUT +from sturdy.mock import MockDendrite from sturdy.pools import generate_challenge_data +from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocationsDict from sturdy.validator.forward import query_and_score_miners -from sturdy.validator.simulator import Simulator +from sturdy.validator.reward import get_rewards +load_dotenv() +os.environ["WEB_PROVIDER_URL"] = "http://127.0.0.1:8545" +WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL") +# TODO: more comprehensive integration testing? class TestValidator(IsolatedAsyncioTestCase): maxDiff = 4000 @classmethod def setUpClass(cls) -> None: - # dont log this in wandb + np.random.seed(69) # noqa: NPY002 config = { "mock": True, "wandb": {"off": True}, "mock_n": 16, "neuron": {"dont_save_events": True}, - "netuid": 69, } + cls.validator = Validator(config=config) - # simulator with preset seed - cls.validator.simulator = Simulator(seed=69) + assert cls.validator.w3.is_connected() + + cls.validator.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + generated_data = generate_challenge_data(cls.validator.w3, np.random.RandomState(seed=420)) + assets_and_pools = generated_data["assets_and_pools"] - assets_and_pools = generate_challenge_data(np.random.RandomState(seed=420)) # type: ignore[] + cls.contract_addresses: list[str] = list(assets_and_pools["pools"].keys()) # type: ignore[] + + assets_and_pools["pools"][cls.contract_addresses[0]].borrow_amount = int(75e18) + assets_and_pools["pools"][cls.contract_addresses[1]].borrow_amount = int(50e18) + assets_and_pools["pools"][cls.contract_addresses[2]].borrow_amount = int(85e18) + assets_and_pools["pools"][cls.contract_addresses[3]].borrow_amount = int(25e18) + assets_and_pools["pools"][cls.contract_addresses[4]].borrow_amount = int(90e18) + assets_and_pools["pools"][cls.contract_addresses[5]].borrow_amount = int(25e18) + assets_and_pools["pools"][cls.contract_addresses[6]].borrow_amount = int(25e18) + assets_and_pools["pools"][cls.contract_addresses[7]].borrow_amount = int(40e18) + assets_and_pools["pools"][cls.contract_addresses[8]].borrow_amount = int(45e18) + assets_and_pools["pools"][cls.contract_addresses[9]].borrow_amount = int(80e18) cls.assets_and_pools = { "pools": assets_and_pools["pools"], "total_assets": int(1000e18), } - cls.contract_addresses = list(assets_and_pools["pools"].keys()) # type: ignore[] - - cls.allocations = { - cls.contract_addresses[0]: 100e18, - cls.contract_addresses[1]: 100e18, - cls.contract_addresses[2]: 200e18, - cls.contract_addresses[3]: 50e18, - cls.contract_addresses[4]: 200e18, - cls.contract_addresses[5]: 25e18, - cls.contract_addresses[6]: 25e18, - cls.contract_addresses[7]: 50e18, - cls.contract_addresses[8]: 50e18, - cls.contract_addresses[9]: 200e18, + cls.allocations: AllocationsDict = { + cls.contract_addresses[0]: int(100e18), + cls.contract_addresses[1]: int(100e18), + cls.contract_addresses[2]: int(200e18), + cls.contract_addresses[3]: int(50e18), + cls.contract_addresses[4]: int(200e18), + cls.contract_addresses[5]: int(45e18), + cls.contract_addresses[6]: int(45e18), + cls.contract_addresses[7]: int(50e18), + cls.contract_addresses[8]: int(50e18), + cls.contract_addresses[9]: int(160e18), } - async def test_query_and_score_miners(self) -> None: - # use simulator generated assets and pools - await query_and_score_miners(self.validator) - self.assertIsNotNone(self.validator.simulator.assets_and_pools) - self.assertIsNotNone(self.validator.simulator.allocations) - self.maxDiff = None - - # use user-defined generated assets and pools - simulator_copy = copy.deepcopy(self.validator.simulator) - await query_and_score_miners( - self.validator, - assets_and_pools=copy.deepcopy(self.assets_and_pools), + async def test_get_rewards(self) -> None: + print("----==== test_get_rewards ====----") + + assets_and_pools = copy.deepcopy(self.assets_and_pools) + allocations = copy.deepcopy(self.allocations) + + validator = self.validator + + validator.simulator.init_data( + init_assets_and_pools=copy.deepcopy(assets_and_pools), + init_allocations=copy.deepcopy(allocations), + ) + + active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] + + active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] + + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=copy.deepcopy(assets_and_pools), + allocations=copy.deepcopy(allocations), + ) + + validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) + responses = await validator.dendrite( + # Send the query to selected miner axons in the network. + axons=active_axons, + # Construct a dummy query. This simply contains a single integer. + synapse=synapse, + deserialize=False, + timeout=QUERY_TIMEOUT, ) - simulator_copy.initialize() - simulator_copy.init_data( - init_assets_and_pools=copy.deepcopy(self.assets_and_pools), + + for response in responses: + self.assertEqual(response.assets_and_pools, assets_and_pools) + self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) + + rewards, allocs = get_rewards( + validator, + validator.step, + active_uids, + responses=responses, + assets_and_pools=assets_and_pools, + ) + + print(f"allocs: {allocs}") + + rewards_dict = {active_uids[k]: v for k, v in enumerate(list(rewards))} + sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + + print(f"sorted rewards: {sorted_rewards}") + + # rewards should not all be the same + to_compare = torch.empty(rewards.shape) + torch.fill(to_compare, rewards[0]) + self.assertFalse(torch.equal(rewards, to_compare)) + + async def test_get_rewards_punish(self) -> None: + print("----==== test_get_rewards_punish ====----") + validator = self.validator + assets_and_pools = copy.deepcopy(self.assets_and_pools) + + allocations = copy.deepcopy(self.allocations) + # increase one of the allocations by +10000 -> clearly this means the miner is cheating!!! + allocations[self.contract_addresses[0]] += int(10000e18) + + validator.simulator.reset() + validator.simulator.init_data( + init_assets_and_pools=copy.deepcopy(assets_and_pools), + init_allocations=copy.deepcopy(allocations), + ) + + active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] + + active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] + + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=copy.deepcopy(assets_and_pools), + allocations=copy.deepcopy(allocations), ) - simulator_copy.update_reserves_with_allocs() - # TODO: update these tests - low priority - # assets_pools_should_be = simulator_copy.assets_and_pools - # assets_pools2 = self.validator.simulator.assets_and_pools - # self.assertEqual(assets_pools2, assets_pools_should_be) - # self.assertIsNotNone(self.validator.simulator.allocations) + validator.dendrite = MockDendrite(wallet=validator.wallet) + responses = await validator.dendrite( + # Send the query to selected miner axons in the network. + axons=active_axons, + # Construct a dummy query. This simply contains a single integer. + synapse=synapse, + deserialize=False, + timeout=QUERY_TIMEOUT, + ) + + for response in responses: + self.assertEqual(response.assets_and_pools, assets_and_pools) + self.assertEqual(response.allocations, allocations) + + rewards, allocs = get_rewards( + validator, + validator.step, + active_uids, + responses=responses, + assets_and_pools=assets_and_pools, + ) + + for allocInfo in allocs.values(): + self.assertEqual(allocInfo["apy"], 0) + + # rewards should all be the same (0) + self.assertEqual(all(rewards), 0) + + rewards_dict = dict(enumerate(list(rewards))) + sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + + print(f"sorted rewards: {sorted_rewards}") + + assets_and_pools = copy.deepcopy(self.assets_and_pools) + + allocations = copy.deepcopy(self.allocations) + # set one of the allocations to be negative! This should not be allowed! + allocations[self.contract_addresses[0]] = -1 + + validator.simulator.reset() + validator.simulator.init_data( + init_assets_and_pools=copy.deepcopy(assets_and_pools), + init_allocations=copy.deepcopy(allocations), + ) + + active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] + + active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] + + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=copy.deepcopy(assets_and_pools), + allocations=copy.deepcopy(allocations), + ) + + validator.dendrite = MockDendrite(wallet=validator.wallet) + responses = await validator.dendrite( + # Send the query to selected miner axons in the network. + axons=active_axons, + # Construct a dummy query. This simply contains a single integer. + synapse=synapse, + deserialize=False, + timeout=QUERY_TIMEOUT, + ) + + for response in responses: + self.assertEqual(response.assets_and_pools, assets_and_pools) + self.assertEqual(response.allocations, allocations) + + rewards, allocs = get_rewards( + validator, + validator.step, + active_uids, + responses=responses, + assets_and_pools=assets_and_pools, + ) + + for allocInfo in allocs.values(): + self.assertEqual(allocInfo["apy"], 0) + + # rewards should all be the same (0) + self.assertEqual(all(rewards), 0) + + rewards_dict = dict(enumerate(list(rewards))) + sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + + print(f"sorted rewards: {sorted_rewards}") + + + async def test_query_and_score_miners(self) -> None: + await query_and_score_miners(self.validator, assets_and_pools=self.assets_and_pools) async def test_forward(self) -> None: await self.validator.forward() diff --git a/tests/unit/validator/test_pool_generator.py b/tests/unit/validator/test_pool_generator.py index ee981ee..6811a5f 100644 --- a/tests/unit/validator/test_pool_generator.py +++ b/tests/unit/validator/test_pool_generator.py @@ -87,10 +87,13 @@ def test_generate_assets_and_pools(self) -> None: generated = assets_pools_for_challenge_data(selected_entry, self.w3) print(generated) + pools = generated["assets_and_pools"]["pools"] + total_assets = generated["assets_and_pools"]["total_assets"] + # check the member variables of the returned value - self.assertEqual(list(generated["pools"].keys()), list(challenge_data["assets_and_pools"]["pools"].keys())) + self.assertEqual(list(pools.keys()), list(pools.keys())) # check returned total assets - self.assertGreater(generated["total_assets"], 0) + self.assertGreater(total_assets, 0) if __name__ == "__main__": diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 4f79943..63acefb 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -116,7 +116,7 @@ def test_sync(self) -> None: ) # sync pool params - pool.sync(self.account.address, web3_provider=self.w3) + pool.sync(web3_provider=self.w3) self.assertTrue(hasattr(pool, "_atoken_contract")) self.assertTrue(isinstance(pool._atoken_contract, Contract)) @@ -132,7 +132,7 @@ def test_supply_rate_alloc(self) -> None: ) # sync pool params - pool.sync(self.account.address, web3_provider=self.w3) + pool.sync(web3_provider=self.w3) reserve_data = retry_with_backoff(pool._pool_contract.functions.getReserveData(pool._underlying_asset_address).call) @@ -149,10 +149,11 @@ def test_supply_rate_decrease_alloc(self) -> None: print("----==== test_supply_rate_decrease_alloc ====----") pool = AaveV3DefaultInterestRatePool( contract_address=self.atoken_address, + user_address=self.account.address ) # sync pool params - pool.sync(self.account.address, web3_provider=self.w3) + pool.sync(web3_provider=self.w3) tx = self.weth_contract.functions.deposit().build_transaction( { @@ -217,7 +218,7 @@ def test_supply_rate_decrease_alloc(self) -> None: print(f"apy before rebalancing ether: {apy_before}") # calculate predicted future supply rate after removing 1000 ETH to end up with 9000 ETH in the pool - pool.sync(self.account.address, self.w3) + pool.sync(self.w3) apy_after = pool.supply_rate(int(9000e18)) print(f"apy after rebalancing ether: {apy_after}") self.assertNotEqual(apy_after, 0) @@ -237,7 +238,8 @@ def setUpClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20233401, + # "blockNumber": 20233401, + "blockNumber": 21080765, }, }, ], @@ -285,13 +287,14 @@ def tearDown(self) -> None: def test_silo_strategy_contract(self) -> None: print("----==== test_pool_contract ====----") - # we call the aave3 weth atoken proxy contract in this example + whale_addr = self.w3.to_checksum_address("0x0669091F451142b3228171aE6aD794cF98288124") + pool = VariableInterestSturdySiloStrategy( contract_address=self.contract_address, + user_address=whale_addr ) # type: ignore[] - whale_addr = self.w3.to_checksum_address("0x0669091F451142b3228171aE6aD794cF98288124") - pool.sync(whale_addr, self.w3) + pool.sync(self.w3) self.assertTrue(hasattr(pool, "_silo_strategy_contract")) self.assertTrue(isinstance(pool._silo_strategy_contract, Contract)) @@ -305,6 +308,11 @@ def test_silo_strategy_contract(self) -> None: self.assertTrue(isinstance(pool._rate_model_contract, Contract)) print(f"rate model contract: {pool._rate_model_contract.address}") + self.assertTrue(hasattr(pool, "_price_per_share")) + self.assertTrue(isinstance(pool._price_per_share, int)) + print(f"price per share: {pool._price_per_share}") + + # don't change deposit amount to pool by much prev_supply_rate = pool.supply_rate(int(630e18)) # increase deposit amount to pool by ~100e18 (~630 pxETH) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index cfe4807..94b82b0 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -14,10 +14,10 @@ adjust_rewards_for_plagiarism, calculate_penalties, calculate_rewards_with_adjusted_penalties, - normalize_squared, format_allocations, get_distance, get_similarity_matrix, + normalize_squared, ) load_dotenv() @@ -211,122 +211,6 @@ def tearDown(self) -> None: # Optional: Revert to the original snapshot after each test self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] - def test_check_allocations_valid(self) -> None: - allocations = {ADDRESS_ZERO: int(5e18), BEEF: int(3e18)} - assets_and_pools = { - "total_assets": int(8e18), - "pools": { - ADDRESS_ZERO: BasePool( - contract_address=ADDRESS_ZERO, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(2e18), - reserve_size=0, - ), - BEEF: BasePool( - contract_address=BEEF, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(1e18), - reserve_size=0, - ), - }, - } - - result = check_allocations(assets_and_pools, allocations) - self.assertTrue(result) - - def test_check_allocations_overallocate(self) -> None: - allocations = {ADDRESS_ZERO: int(10e18), BEEF: int(3e18)} - assets_and_pools = { - "total_assets": int(10e18), - "pools": { - ADDRESS_ZERO: BasePool( - contract_address=ADDRESS_ZERO, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(2e18), - reserve_size=0, - ), - BEEF: BasePool( - contract_address=BEEF, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(1e18), - reserve_size=0, - ), - }, - } - - result = check_allocations(assets_and_pools, allocations) - self.assertFalse(result) - - def test_check_allocations_below_borrow(self) -> None: - allocations = {ADDRESS_ZERO: int(1e18), BEEF: 0} - assets_and_pools = { - "total_assets": int(10e18), - "pools": { - ADDRESS_ZERO: BasePool( - contract_address=ADDRESS_ZERO, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(2e18), - reserve_size=0, - ), - BEEF: BasePool( - contract_address=BEEF, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(1e18), - reserve_size=0, - ), - }, - } - - result = check_allocations(assets_and_pools, allocations) - self.assertFalse(result) - - def test_check_allocations_below_alloc_threshold(self) -> None: - allocations = {ADDRESS_ZERO: int(4e18), BEEF: int(4e18)} - assets_and_pools = { - "total_assets": int(10e18), - "pools": { - ADDRESS_ZERO: BasePool( - contract_address=ADDRESS_ZERO, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(2e18), - reserve_size=0, - ), - BEEF: BasePool( - contract_address=BEEF, - base_rate=0, - base_slope=0, - kink_slope=0, - optimal_util_rate=0, - borrow_amount=int(1e18), - reserve_size=0, - ), - }, - } - - result = check_allocations(assets_and_pools, allocations) - self.assertFalse(result) - def test_check_allocations_sturdy(self) -> None: A = "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227" VAULT = "0x73E4C11B670Ef9C025A030A20b72CB9150E54523" @@ -343,7 +227,7 @@ def test_check_allocations_sturdy(self) -> None: } pool_a: VariableInterestSturdySiloStrategy = assets_and_pools["pools"][A] - pool_a.sync(VAULT, web3_provider=self.w3) + pool_a.sync(web3_provider=self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available pool_a._totalAssets = int(100e23) @@ -406,7 +290,7 @@ def test_check_allocations_aave(self) -> None: } pool_a: AaveV3DefaultInterestRatePool = assets_and_pools["pools"][A] - pool_a.sync(ADDRESS_ZERO, self.w3) + pool_a.sync(self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available pool_a._total_supplied = int(100e6) diff --git a/tests/unit/validator/test_validator.py b/tests/unit/validator/test_validator.py deleted file mode 100644 index a3afe41..0000000 --- a/tests/unit/validator/test_validator.py +++ /dev/null @@ -1,237 +0,0 @@ -import copy -import unittest -from unittest import IsolatedAsyncioTestCase - -import numpy as np -import torch - -from neurons.validator import Validator -from sturdy.constants import QUERY_TIMEOUT -from sturdy.mock import MockDendrite -from sturdy.pools import generate_challenge_data -from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocationsDict -from sturdy.validator.reward import get_rewards - - -class TestValidator(IsolatedAsyncioTestCase): - @classmethod - def setUpClass(cls) -> None: - np.random.seed(69) # noqa: NPY002 - config = { - "mock": True, - "wandb": {"off": True}, - "mock_n": 16, - "neuron": {"dont_save_events": True}, - } - cls.validator = Validator(config=config) - # TODO: this doesn't work? - # cls.validator.simulator = Simulator(69) - - assets_and_pools = generate_challenge_data(np.random.RandomState(seed=420)) - - cls.contract_addresses: list[str] = list(assets_and_pools["pools"].keys()) # type: ignore[] - - assets_and_pools["pools"][cls.contract_addresses[0]].borrow_amount = int(75e18) - assets_and_pools["pools"][cls.contract_addresses[1]].borrow_amount = int(50e18) - assets_and_pools["pools"][cls.contract_addresses[2]].borrow_amount = int(85e18) - assets_and_pools["pools"][cls.contract_addresses[3]].borrow_amount = int(25e18) - assets_and_pools["pools"][cls.contract_addresses[4]].borrow_amount = int(90e18) - assets_and_pools["pools"][cls.contract_addresses[5]].borrow_amount = int(25e18) - assets_and_pools["pools"][cls.contract_addresses[6]].borrow_amount = int(25e18) - assets_and_pools["pools"][cls.contract_addresses[7]].borrow_amount = int(40e18) - assets_and_pools["pools"][cls.contract_addresses[8]].borrow_amount = int(45e18) - assets_and_pools["pools"][cls.contract_addresses[9]].borrow_amount = int(80e18) - - cls.assets_and_pools = { - "pools": assets_and_pools["pools"], - "total_assets": int(1000e18), - } - - cls.allocations: AllocationsDict = { - cls.contract_addresses[0]: int(100e18), - cls.contract_addresses[1]: int(100e18), - cls.contract_addresses[2]: int(200e18), - cls.contract_addresses[3]: int(50e18), - cls.contract_addresses[4]: int(200e18), - cls.contract_addresses[5]: int(45e18), - cls.contract_addresses[6]: int(45e18), - cls.contract_addresses[7]: int(50e18), - cls.contract_addresses[8]: int(50e18), - cls.contract_addresses[9]: int(160e18), - } - - cls.validator.simulator.initialize(timesteps=50) - - async def test_get_rewards(self) -> None: - print("----==== test_get_rewards ====----") - - assets_and_pools = copy.deepcopy(self.assets_and_pools) - allocations = copy.deepcopy(self.allocations) - - validator = self.validator - - validator.simulator.init_data( - init_assets_and_pools=copy.deepcopy(assets_and_pools), - init_allocations=copy.deepcopy(allocations), - ) - - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] - - active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] - - synapse = AllocateAssets( - request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy.deepcopy(assets_and_pools), - allocations=copy.deepcopy(allocations), - ) - - validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) - responses = await validator.dendrite( - # Send the query to selected miner axons in the network. - axons=active_axons, - # Construct a dummy query. This simply contains a single integer. - synapse=synapse, - deserialize=False, - timeout=QUERY_TIMEOUT, - ) - - for response in responses: - self.assertEqual(response.assets_and_pools, assets_and_pools) - self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) - - rewards, allocs = get_rewards( - validator, - validator.step, - active_uids, - responses=responses, - assets_and_pools=assets_and_pools, - ) - - print(f"allocs: {allocs}") - - rewards_dict = {active_uids[k]: v for k, v in enumerate(list(rewards))} - sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] - - print(f"sorted rewards: {sorted_rewards}") - - # rewards should not all be the same - to_compare = torch.empty(rewards.shape) - torch.fill(to_compare, rewards[0]) - self.assertFalse(torch.equal(rewards, to_compare)) - - async def test_get_rewards_punish(self) -> None: - print("----==== test_get_rewards_punish ====----") - validator = self.validator - assets_and_pools = copy.deepcopy(self.assets_and_pools) - - allocations = copy.deepcopy(self.allocations) - # increase one of the allocations by +10000 -> clearly this means the miner is cheating!!! - allocations[self.contract_addresses[0]] += int(10000e18) - - validator.simulator.reset() - validator.simulator.init_data( - init_assets_and_pools=copy.deepcopy(assets_and_pools), - init_allocations=copy.deepcopy(allocations), - ) - - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] - - active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] - - synapse = AllocateAssets( - request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy.deepcopy(assets_and_pools), - allocations=copy.deepcopy(allocations), - ) - - validator.dendrite = MockDendrite(wallet=validator.wallet) - responses = await validator.dendrite( - # Send the query to selected miner axons in the network. - axons=active_axons, - # Construct a dummy query. This simply contains a single integer. - synapse=synapse, - deserialize=False, - timeout=QUERY_TIMEOUT, - ) - - for response in responses: - self.assertEqual(response.assets_and_pools, assets_and_pools) - self.assertEqual(response.allocations, allocations) - - rewards, allocs = get_rewards( - validator, - validator.step, - active_uids, - responses=responses, - assets_and_pools=assets_and_pools, - ) - - for allocInfo in allocs.values(): - self.assertEqual(allocInfo["apy"], 0) - - # rewards should all be the same (0) - self.assertEqual(all(rewards), 0) - - rewards_dict = dict(enumerate(list(rewards))) - sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] - - print(f"sorted rewards: {sorted_rewards}") - - assets_and_pools = copy.deepcopy(self.assets_and_pools) - - allocations = copy.deepcopy(self.allocations) - # set one of the allocations to be negative! This should not be allowed! - allocations[self.contract_addresses[0]] = -1 - - validator.simulator.reset() - validator.simulator.init_data( - init_assets_and_pools=copy.deepcopy(assets_and_pools), - init_allocations=copy.deepcopy(allocations), - ) - - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] - - active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] - - synapse = AllocateAssets( - request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy.deepcopy(assets_and_pools), - allocations=copy.deepcopy(allocations), - ) - - validator.dendrite = MockDendrite(wallet=validator.wallet) - responses = await validator.dendrite( - # Send the query to selected miner axons in the network. - axons=active_axons, - # Construct a dummy query. This simply contains a single integer. - synapse=synapse, - deserialize=False, - timeout=QUERY_TIMEOUT, - ) - - for response in responses: - self.assertEqual(response.assets_and_pools, assets_and_pools) - self.assertEqual(response.allocations, allocations) - - rewards, allocs = get_rewards( - validator, - validator.step, - active_uids, - responses=responses, - assets_and_pools=assets_and_pools, - ) - - for allocInfo in allocs.values(): - self.assertEqual(allocInfo["apy"], 0) - - # rewards should all be the same (0) - self.assertEqual(all(rewards), 0) - - rewards_dict = dict(enumerate(list(rewards))) - sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] - - print(f"sorted rewards: {sorted_rewards}") - - -if __name__ == "__main__": - unittest.main() From 228dc6d4bcf270393b481b527b93cb5911edf7f6 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 6 Nov 2024 00:29:44 +0000 Subject: [PATCH 09/51] chore: some small cleanup + organic requests --- sturdy/mock.py | 3 +- sturdy/protocol.py | 1 - sturdy/validator/forward.py | 17 ++- sturdy/validator/reward.py | 2 - .../validator/test_integration_validator.py | 104 +++++++----------- 5 files changed, 55 insertions(+), 72 deletions(-) diff --git a/sturdy/mock.py b/sturdy/mock.py index 9f3cf2b..fdece88 100644 --- a/sturdy/mock.py +++ b/sturdy/mock.py @@ -7,6 +7,7 @@ import numpy as np from sturdy.constants import QUERY_TIMEOUT +from sturdy.pools import get_minimum_allocation def generate_array_with_sum(rng_gen: np.random.RandomState, total_sum: int, min_amounts: [int]) -> list: @@ -113,7 +114,7 @@ async def single_axon_response(i, axon): # noqa: ANN202, ARG001 if self.custom_allocs: pools = synapse.assets_and_pools["pools"] - min_amounts = [pool.borrow_amount for pool in pools.values()] + min_amounts = [get_minimum_allocation(pool) for pool in pools.values()] alloc_values = generate_array_with_sum(np.random, s.assets_and_pools["total_assets"], min_amounts) contract_addrs = [pool.contract_address for pool in s.assets_and_pools["pools"].values()] diff --git a/sturdy/protocol.py b/sturdy/protocol.py index 0b02e49..d985076 100644 --- a/sturdy/protocol.py +++ b/sturdy/protocol.py @@ -37,7 +37,6 @@ class REQUEST_TYPES(IntEnum): class AllocInfo(TypedDict): - apy: int allocations: AllocationsDict | None diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 1dbe8c9..fc9fde1 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -133,6 +133,10 @@ async def query_and_score_miners( bt.logging.debug(f"Assets and pools: {synapse.assets_and_pools}") bt.logging.debug(f"Received allocations (uid -> allocations): {allocations}") + curr_pools = assets_and_pools["pools"] + for pool in curr_pools.values(): + pool.sync(self.w3) + # score previously suggested miner allocations based on how well they are performing now # get all the request ids for the pools we should be scoring from the db @@ -167,5 +171,16 @@ async def query_and_score_miners( ) # TODO: sort the miners' by their current scores and return their respective allocations + sorted_indices = [idx for idx, val in sorted(enumerate(self.scores), key=lambda k: k[1], reverse=True)] + + sorted_allocs = {} + for idx in sorted_indices: + alloc = filtered_allocs.get(str(idx), None) + if alloc is None: + continue + + sorted_allocs[str(idx)] = alloc + + bt.logging.debug(f"sorted allocations: {sorted_allocs}") - return axon_times, filtered_allocs + return axon_times, sorted_allocs diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index e628ba8..2913b11 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -335,7 +335,6 @@ def filter_allocations( for response_idx, response in enumerate(responses): allocations = response.allocations - # validator miner allocations before running simulation # is the miner cheating w.r.t allocations? cheating = True try: @@ -361,7 +360,6 @@ def filter_allocations( sorted_axon_times = dict(sorted(axon_times.items(), key=lambda item: item[1])) bt.logging.debug(f"sorted axon times:\n{sorted_axon_times}") - bt.logging.debug(f"sorted filtered allocs:\n{curr_filtered_allocs}") self.sorted_axon_times = sorted_axon_times diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index 65b18bb..bbae840 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -1,23 +1,25 @@ -import copy import os import unittest +from copy import copy from unittest import IsolatedAsyncioTestCase import numpy as np import torch from dotenv import load_dotenv +from web3 import Web3 from neurons.validator import Validator +from sturdy.algo import naive_algorithm from sturdy.constants import QUERY_TIMEOUT from sturdy.mock import MockDendrite from sturdy.pools import generate_challenge_data -from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocationsDict +from sturdy.protocol import REQUEST_TYPES, AllocateAssets from sturdy.validator.forward import query_and_score_miners from sturdy.validator.reward import get_rewards load_dotenv() +EXTERNAL_WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL") os.environ["WEB_PROVIDER_URL"] = "http://127.0.0.1:8545" -WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL") # TODO: more comprehensive integration testing? class TestValidator(IsolatedAsyncioTestCase): @@ -34,6 +36,8 @@ def setUpClass(cls) -> None: } cls.validator = Validator(config=config) + w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + cls.validator.w3 = w3 assert cls.validator.w3.is_connected() cls.validator.w3.provider.make_request( @@ -41,7 +45,7 @@ def setUpClass(cls) -> None: [ { "forking": { - "jsonRpcUrl": WEB3_PROVIDER_URL, + "jsonRpcUrl": EXTERNAL_WEB3_PROVIDER_URL, "blockNumber": 21080765, }, }, @@ -49,62 +53,36 @@ def setUpClass(cls) -> None: ) generated_data = generate_challenge_data(cls.validator.w3, np.random.RandomState(seed=420)) - assets_and_pools = generated_data["assets_and_pools"] - - cls.contract_addresses: list[str] = list(assets_and_pools["pools"].keys()) # type: ignore[] - - assets_and_pools["pools"][cls.contract_addresses[0]].borrow_amount = int(75e18) - assets_and_pools["pools"][cls.contract_addresses[1]].borrow_amount = int(50e18) - assets_and_pools["pools"][cls.contract_addresses[2]].borrow_amount = int(85e18) - assets_and_pools["pools"][cls.contract_addresses[3]].borrow_amount = int(25e18) - assets_and_pools["pools"][cls.contract_addresses[4]].borrow_amount = int(90e18) - assets_and_pools["pools"][cls.contract_addresses[5]].borrow_amount = int(25e18) - assets_and_pools["pools"][cls.contract_addresses[6]].borrow_amount = int(25e18) - assets_and_pools["pools"][cls.contract_addresses[7]].borrow_amount = int(40e18) - assets_and_pools["pools"][cls.contract_addresses[8]].borrow_amount = int(45e18) - assets_and_pools["pools"][cls.contract_addresses[9]].borrow_amount = int(80e18) - - cls.assets_and_pools = { - "pools": assets_and_pools["pools"], - "total_assets": int(1000e18), - } + cls.assets_and_pools = generated_data["assets_and_pools"] - cls.allocations: AllocationsDict = { - cls.contract_addresses[0]: int(100e18), - cls.contract_addresses[1]: int(100e18), - cls.contract_addresses[2]: int(200e18), - cls.contract_addresses[3]: int(50e18), - cls.contract_addresses[4]: int(200e18), - cls.contract_addresses[5]: int(45e18), - cls.contract_addresses[6]: int(45e18), - cls.contract_addresses[7]: int(50e18), - cls.contract_addresses[8]: int(50e18), - cls.contract_addresses[9]: int(160e18), - } + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=copy(cls.assets_and_pools), + ) + + cls.allocations = naive_algorithm(cls.validator, synapse) + + cls.contract_addresses: list[str] = list(cls.assets_and_pools["pools"].keys()) # type: ignore[] async def test_get_rewards(self) -> None: print("----==== test_get_rewards ====----") - assets_and_pools = copy.deepcopy(self.assets_and_pools) - allocations = copy.deepcopy(self.allocations) + assets_and_pools = copy(self.assets_and_pools) + allocations = copy(self.allocations) validator = self.validator - validator.simulator.init_data( - init_assets_and_pools=copy.deepcopy(assets_and_pools), - init_allocations=copy.deepcopy(allocations), - ) - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] synapse = AllocateAssets( request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy.deepcopy(assets_and_pools), - allocations=copy.deepcopy(allocations), + assets_and_pools=copy(assets_and_pools), + allocations=copy(allocations), ) + validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) responses = await validator.dendrite( # Send the query to selected miner axons in the network. @@ -115,8 +93,10 @@ async def test_get_rewards(self) -> None: timeout=QUERY_TIMEOUT, ) + for response in responses: - self.assertEqual(response.assets_and_pools, assets_and_pools) + # TODO: is this necessary? + # self.assertEqual(response.assets_and_pools, self.assets_and_pools) self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) rewards, allocs = get_rewards( @@ -142,26 +122,20 @@ async def test_get_rewards(self) -> None: async def test_get_rewards_punish(self) -> None: print("----==== test_get_rewards_punish ====----") validator = self.validator - assets_and_pools = copy.deepcopy(self.assets_and_pools) + assets_and_pools = copy(self.assets_and_pools) - allocations = copy.deepcopy(self.allocations) + allocations = copy(self.allocations) # increase one of the allocations by +10000 -> clearly this means the miner is cheating!!! allocations[self.contract_addresses[0]] += int(10000e18) - validator.simulator.reset() - validator.simulator.init_data( - init_assets_and_pools=copy.deepcopy(assets_and_pools), - init_allocations=copy.deepcopy(allocations), - ) - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] synapse = AllocateAssets( request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy.deepcopy(assets_and_pools), - allocations=copy.deepcopy(allocations), + assets_and_pools=copy(assets_and_pools), + allocations=copy(allocations), ) validator.dendrite = MockDendrite(wallet=validator.wallet) @@ -175,7 +149,8 @@ async def test_get_rewards_punish(self) -> None: ) for response in responses: - self.assertEqual(response.assets_and_pools, assets_and_pools) + # TODO: is this necessary? + # self.assertEqual(response.assets_and_pools, assets_and_pools) self.assertEqual(response.allocations, allocations) rewards, allocs = get_rewards( @@ -197,26 +172,20 @@ async def test_get_rewards_punish(self) -> None: print(f"sorted rewards: {sorted_rewards}") - assets_and_pools = copy.deepcopy(self.assets_and_pools) + assets_and_pools = copy(self.assets_and_pools) - allocations = copy.deepcopy(self.allocations) + allocations = copy(self.allocations) # set one of the allocations to be negative! This should not be allowed! allocations[self.contract_addresses[0]] = -1 - validator.simulator.reset() - validator.simulator.init_data( - init_assets_and_pools=copy.deepcopy(assets_and_pools), - init_allocations=copy.deepcopy(allocations), - ) - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] synapse = AllocateAssets( request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy.deepcopy(assets_and_pools), - allocations=copy.deepcopy(allocations), + assets_and_pools=copy(assets_and_pools), + allocations=copy(allocations), ) validator.dendrite = MockDendrite(wallet=validator.wallet) @@ -230,7 +199,8 @@ async def test_get_rewards_punish(self) -> None: ) for response in responses: - self.assertEqual(response.assets_and_pools, assets_and_pools) + # TODO: is this necessary? + # self.assertEqual(response.assets_and_pools, assets_and_pools) self.assertEqual(response.allocations, allocations) rewards, allocs = get_rewards( From e30b5729ad892e1ff48af38cfe28488533783049 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 6 Nov 2024 00:30:10 +0000 Subject: [PATCH 10/51] chore: ruff format --- sturdy/validator/reward.py | 4 +--- sturdy/validator/sql.py | 2 +- .../validator/test_integration_validator.py | 4 +--- tests/unit/validator/test_pool_models.py | 11 ++--------- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 2913b11..ec4b9ac 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -258,9 +258,7 @@ def _get_rewards( def calculate_apy( - allocations: AllocationsDict, - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], - extra_metadata: dict + allocations: AllocationsDict, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], extra_metadata: dict ) -> int: """ Calculates immediate projected yields given intial assets and pools, pool history, and number of timesteps diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index b3f8987..de7bba2 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -172,7 +172,7 @@ def log_allocations( datetime_now, request_type, # TODO: use jsonable_encoder? - json.dumps(extra_metadata) + json.dumps(extra_metadata), ), ) diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index bbae840..b8499c6 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -21,6 +21,7 @@ EXTERNAL_WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL") os.environ["WEB_PROVIDER_URL"] = "http://127.0.0.1:8545" + # TODO: more comprehensive integration testing? class TestValidator(IsolatedAsyncioTestCase): maxDiff = 4000 @@ -82,7 +83,6 @@ async def test_get_rewards(self) -> None: allocations=copy(allocations), ) - validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) responses = await validator.dendrite( # Send the query to selected miner axons in the network. @@ -93,7 +93,6 @@ async def test_get_rewards(self) -> None: timeout=QUERY_TIMEOUT, ) - for response in responses: # TODO: is this necessary? # self.assertEqual(response.assets_and_pools, self.assets_and_pools) @@ -222,7 +221,6 @@ async def test_get_rewards_punish(self) -> None: print(f"sorted rewards: {sorted_rewards}") - async def test_query_and_score_miners(self) -> None: await query_and_score_miners(self.validator, assets_and_pools=self.assets_and_pools) diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 63acefb..d93f451 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -147,10 +147,7 @@ def test_supply_rate_alloc(self) -> None: def test_supply_rate_decrease_alloc(self) -> None: print("----==== test_supply_rate_decrease_alloc ====----") - pool = AaveV3DefaultInterestRatePool( - contract_address=self.atoken_address, - user_address=self.account.address - ) + pool = AaveV3DefaultInterestRatePool(contract_address=self.atoken_address, user_address=self.account.address) # sync pool params pool.sync(web3_provider=self.w3) @@ -289,10 +286,7 @@ def test_silo_strategy_contract(self) -> None: print("----==== test_pool_contract ====----") whale_addr = self.w3.to_checksum_address("0x0669091F451142b3228171aE6aD794cF98288124") - pool = VariableInterestSturdySiloStrategy( - contract_address=self.contract_address, - user_address=whale_addr - ) # type: ignore[] + pool = VariableInterestSturdySiloStrategy(contract_address=self.contract_address, user_address=whale_addr) # type: ignore[] pool.sync(self.w3) @@ -312,7 +306,6 @@ def test_silo_strategy_contract(self) -> None: self.assertTrue(isinstance(pool._price_per_share, int)) print(f"price per share: {pool._price_per_share}") - # don't change deposit amount to pool by much prev_supply_rate = pool.supply_rate(int(630e18)) # increase deposit amount to pool by ~100e18 (~630 pxETH) From 80221d200f364b27bf5a4c73256c6de7a387ecb4 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 6 Nov 2024 04:10:06 +0000 Subject: [PATCH 11/51] feat: update scoring interval, query interval + update docs --- README.md | 148 +++++++++---------- assets/subnet_architecture.png | Bin 0 -> 601736 bytes db/migrations/20241030231410_alloc_table.sql | 2 +- docs/validator.md | 21 +-- neurons/validator.py | 5 +- sturdy/base/validator.py | 10 +- sturdy/constants.py | 9 +- sturdy/validator/forward.py | 23 ++- sturdy/validator/reward.py | 25 ---- 9 files changed, 114 insertions(+), 129 deletions(-) create mode 100644 assets/subnet_architecture.png diff --git a/README.md b/README.md index 6213a80..58042b1 100644 --- a/README.md +++ b/README.md @@ -49,68 +49,71 @@ There are three core files. ### Subnet Overview - Validators are responsible for distributing lists of pools (of which contain relevant parameters such as base interest rate, base interest rate slope, minimum borrow amount, etc), as well as a - maximum token balance miners can allocate to pools. Below is the function present in the codebase - used for generating a dummy `assets_and_pools` taken from [pools.py](./sturdy/pools.py) used for - synthetic requests: + maximum token balance miners can allocate to pools. Below are the function present in the codebase + used for generating challenge data in [pools.py](./sturdy/pools.py) used for + synthetic requests. The selection of different assets and pools which can be used in such requests are defined in the [pool registry](./sturdy/pool_registry/pool_registry.py), and are all based on pools which are real and do indeed exist on-chain (i.e. on the Ethereum Mainnet): ```python - def generate_eth_public_key(rng_gen: np.random.RandomState) -> str: - private_key_bytes = rng_gen.bytes(32) # type: ignore[] - account = Account.from_key(private_key_bytes) - return account.address - - - def generate_assets_and_pools(rng_gen: np.random.RandomState) -> dict[str, dict[str, BasePoolModel] | int]: # generate pools - assets_and_pools = {} - - pools_list = [ - BasePool( - contract_address=generate_eth_public_key(rng_gen=rng_gen), - pool_type=POOL_TYPES.SYNTHETIC, - base_rate=int(randrange_float(MIN_BASE_RATE, MAX_BASE_RATE, BASE_RATE_STEP, rng_gen=rng_gen)), - base_slope=int(randrange_float(MIN_SLOPE, MAX_SLOPE, SLOPE_STEP, rng_gen=rng_gen)), - kink_slope=int( - randrange_float(MIN_KINK_SLOPE, MAX_KINK_SLOPE, SLOPE_STEP, rng_gen=rng_gen), - ), # kink rate - kicks in after pool hits optimal util rate - optimal_util_rate=int( - randrange_float( - MIN_OPTIMAL_RATE, - MAX_OPTIMAL_RATE, - OPTIMAL_UTIL_STEP, - rng_gen=rng_gen, - ), - ), # optimal util rate - after which the kink slope kicks in - borrow_amount=int( - format_num_prec( - wei_mul( - POOL_RESERVE_SIZE, - int( - randrange_float( - MIN_UTIL_RATE, - MAX_UTIL_RATE, - UTIL_RATE_STEP, - rng_gen=rng_gen, - ), - ), - ), - ), - ), # initial borrowed amount from pool - reserve_size=int(POOL_RESERVE_SIZE), - ) - for _ in range(NUM_POOLS) - ] + def generate_challenge_data( + web3_provider: Web3, + rng_gen: np.random.RandomState = np.random.RandomState(), + ) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools + selected_entry = POOL_REGISTRY[rng_gen.choice(list(POOL_REGISTRY.keys()))] + bt.logging.debug(f"Selected pool registry entry: {selected_entry}") - pools = {str(pool.contract_address): pool for pool in pools_list} + return assets_pools_for_challenge_data(selected_entry, web3_provider) - minimums = [pool.borrow_amount for pool in pools_list] - min_total = sum(minimums) - assets_and_pools["total_assets"] = int(min_total) + int( - math.floor( - randrange_float(MIN_TOTAL_ASSETS_OFFSET, MAX_TOTAL_ASSETS_OFFSET, TOTAL_ASSETS_OFFSET_STEP, rng_gen=rng_gen), - ) - ) - assets_and_pools["pools"] = pools - return assets_and_pools + def assets_pools_for_challenge_data( + selected_entry, web3_provider: Web3 + ) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools + challenge_data = {} + + selected_assets_and_pools = selected_entry["assets_and_pools"] + selected_pools = selected_assets_and_pools["pools"] + user_address = selected_entry["user_address"] + + pool_list = [] + + for pool_dict in selected_pools.values(): + pool = PoolFactory.create_pool( + pool_type=POOL_TYPES._member_map_[pool_dict["pool_type"]], + pool_model_disc=pool_dict["pool_model_disc"], + user_address=user_address, + contract_address=pool_dict["contract_address"], + ) + pool_list.append(pool) + + pools = {str(pool.contract_address): pool for pool in pool_list} + + # we assume that the user address is the same across pools (valid) + # and also that the asset contracts are the same across said pools + first_pool = pool_list[0] + total_assets = 0 + + match first_pool.pool_type: + case POOL_TYPES.STURDY_SILO: + first_pool.sync(web3_provider) + total_assets = first_pool._user_asset_balance + case _: + pass + + for pool in pools.values(): + pool.sync(web3_provider) + total_asset = 0 + match pool.pool_type: + case POOL_TYPES.STURDY_SILO: + total_asset += pool._curr_deposit_amount + case _: + pass + + total_assets += total_asset + + challenge_data["assets_and_pools"] = {} + challenge_data["assets_and_pools"]["pools"] = pools + challenge_data["assets_and_pools"]["total_assets"] = total_assets + challenge_data["user_address"] = user_address + + return challenge_data ``` Validators can optionally run an API server and sell their bandwidth to outside users to send their own pools (organic requests) to the subnet. For more information on this process - please read @@ -125,34 +128,19 @@ There are three core files. [algo.py](./sturdy/algo.py). The naive allocation essentially works by divvying assets across pools, and allocating more to pools which have a higher current supply rate. -- After generating allocations, miners then send their outputs to validators to be scored. For - synthetic requests, validators run a simulation which simulates borrow behavior over a predetermined - amount of timesteps. For organic requests, on the other hand, validators query the relevant smart - contracts of user-defined pools on the Ethereum Network to calculate the miners' allocation's - yields. The scores of miners are determined based on their relative aggregate - yields, and miners which have similar allocations to other miners will be penalized if they are +- After generating allocations, miners then send their outputs to validators to be scored. These requests are generated and sent to miners roughly every 15 minutes. + Organic requests, on the other hand, are sent by to validators, upon which they are then routed to miners. After the "scoring period" for requests have passed, miners are then scored based on how much yield pools have generated within the scoring period - with the miner with the most yield obtaining the highest score. Scoring these miners involves gather on chain info about pools, with most if not all such information being obtained from smart contracts on the the Ethereum Network. Miners which have similar allocations to other miners will be penalized if they are not perceived as being original. If miners fail to respond in ~45 seconds after receiving the - request they are scored - poorly. + request they are scored poorly. The best allocating miner will receive the most emissions. For more information on how - miners are rewarded and how the simulator works- please see - [reward.py](sturdy/validator/reward.py) and [simulator.py](sturdy/validator/simulator.py) - respectively. A diagram is provided below highlighting the interactions that takes place within - the subnet when processing organic requests: + miners are rewarded - please see + [reward.py](sturdy/validator/reward.py). A diagram is provided below highlighting the interactions that takes place within + the subnet when processing synthetic and organic requests:
- +
- - -- We provide a demo which plots simulations in [plot_simulator.py](demos/plot_simulator.py). We - provide a sample output of the script below: - -
- -
- --- ## Installation diff --git a/assets/subnet_architecture.png b/assets/subnet_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..a624d8039d4374422a8bb68ad3a658f02d647074 GIT binary patch literal 601736 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� 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⁣J}lX2tnUvA*@ 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

Qm1XvoUUQVnEU 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

E7no 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

H!pj|pMP82^1uQWNIEy_LEQdbr2zor?$CL|y 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∈EQNvP7_y`|q|%bb^O(~& zMKwEx6OO#@DKb(s%x)0wNj0t*MfZHYOxT!?lEmfre!|z->U-cB)PL&s3*(7b)9fXK zv$)SN;4(aYT7oeD;M=WA%84Hz1I8eX(~aqZUcWi9h7-s z&8@rS`a#+jG$XL0Xvj8lKlwwIv!;cSHLw{)RhaCh$S&Iqb)S^7IaB~M_yZ(1k?8|6 zx+Z8pbQV*!xYA@xuH_&`sl(7+@Zd7&PdwQSeL}{urcI>ca0V{+$y}IBCp%{hg27^$ zGwi18G^P<0xU}VtLgiNl8v$M@iYCi^n1Id2+?-QptT)P$Zbl~?sHDEtalU6kr(=eH>IWl|_Mv1|*Yv6l71TJ$D6C{qj(MKILJ^%A#_r>hRQqSa-{NC|9RmlrGMJs!7ETQ6v z+l^M8(Pc9n6B4sApOE^x7O!Bm!7M2@D(WyM;UZVj1c!NS0>tOGhTWo%2=nf7o-rljf*g*c~ z)H-IZS35VdZS{vd;G;*9(@5sF^ei=YVf2hF?4vJV{?H17pFuYc}! z8^mLN_sPH0m}kg)_`$XG9S|m_!{~Z?=Zy!iGbBX9vNBB>nd@jzYHfIo-;W>Z(IZN; zgEvAkuA3*jIR)HC7IE7b$@$0=7*{MsO*V$j%QW3Gc0(T&R%e=#>9O8uB2WO0>rf1a zu1hWM0qK?uw?G)qr3&L_&mk@(Pe5?%jv&o+(^XeF_JrD3Y(fRlqPDk39PYiC+9SD9 z?{`nWS|X1*;(*5871sTtM02~EaFy$TX*b)XH zwkYrUy3VG2{d)-B@L&2SZ7>a1ZI54Z4nE$A_wX2sXoDX+cH!?_Xh^lJbYBMz9S;i|3ZU;L1Tp1^Pp$x$7q6r!gdzxeYTF; zo+9vBM>+Gp7SCdNb$XFYi=17CBs4eecOQGBf3?XngE?s;Ehnk&bl1;c&bmqcMYg9- zJ>Rh*|A1Okw+ykxw$V4MO*eTBqf7ED;#!GI@7vWl#Cj zn~qLS9lgErtJ%bi@xPFSGF@9m_frQN-O$M5bt8raWQ1J8zX5f7OZ35rXA%ooW9 z*0ZrCpn@ZVB+6DB2YvEj`wI*74;SbFl9Sos4b04^2PEoVoK}lvAyuvam6S>zCvSE( zW|du!tK!kihBS5dPRLGByLA2I1R7#O;&`;nplEHwthO%XBejc;S`HCVJ)l z^9fHcuWCZ?#^Dp~Csc``M~}KI9BM@Hf==;c$^x8+tNzYx-zOb=ri|K6_3bcRvXN(z zsV)?xVx5rvkOMWY8 z<6YI=*FfIZ(9O~PwM7$wqYhL`?a*H#m;gCU_`E1GZNNqCj*Cy+>w^m67&N3RA#;;z z(|Z!N_3o%C{627x!Yyp(qc(bAOuLIE){uF%tS(-J>l?Q?~-e zH@j~O%~0xheb=m}ptm(u-`QwVue)vM;fbrx^WU-rOf?LS0(C{!eqU4*WmcwCtIhAu zKV@ZFJV-J{>G9E14wX>T9K)a!lL7t=xdc9q%whygxqVmS>t~;=f-c}y5FcNBkq8Vg zxJ15Ng`uZPFdULlVjV@np>+Fa4hXqIK-0Irzl1L@0Zfvt1T;|-kfpRYVFF;+&Yd4R z4q!D@P>fYWNDZ^L<@HjRzsp6_-z-3`di3_XyP#l2!nNSpip}8oy?3$09Bt zCo&8uw~(y|g@p87S#-cz^S}p{sxT#+z!wRLwmwt`-quVsYOGi*|8NTkGz}9G1%J#W zH(?iDs#MLYpAKqArSfP*TbZwBuH7h#9*eWC=abvka2iWa>@H_ z5@;F35@tI&t-k$#Y`qCQ)oa^5u8~A@8Ka~_6eS|c)QFN<$&jQBna9i}DrHDXC-Xd$ zGS5*|iexI=m}JUKrtn)g^*+!0{{Q=P&U4PwVejw$e((FbuWPM!t;@qBChuy7abs0i z8H{A9sLs7>%1)4NoX-B|ty^2y_OUZ|K7J^kyoey(U>M@@IE>ZkM|P?4@~ZbbcJZGz z(~T(?s;nw@q!EK@nS1H<*fFb|AVnV+R$)kkuyg#H0(i&y27SDW zRtTO$hqBa+;^FKclx~ohx|-?3RjUNo*WAYZO{QE$RZrDr*$((i&$)N8mT zQl>I!=F`l%bX^CMbhfYse>uJyfaNxw-onhYK5R!5PqE!N4u6EihGr0A@(b&}o2D1X zk=+S%#Lp;26Ja5v4S|1D?Ff2jh>d|AOY&23=REW|^2o@dm6}ipDCWdMHZw6Lz;8sB z$01~pZK^i;!a==!cvq0cjVv&oAXqbS9F4@7YCRnkTv}B&=;aslU_>a5!P=HY=~jc# zvbcr`CI$C-2?KO7%QxorGj3#OPb1Jmr=Q;j((HyPYSdO#SZPe2M+pUS{c&NoQN+`4 zr8AG&mz$kgT-_e9i7`aHn?d;N8_Bh@w1g$toD2abP_B262c{_`yvY_CQGpXsBtm?k z749}@rqCbCt1BdIMV9ZdIeXTw9$gB=9A)F}g!@Cwm;CP9PxJi1HD*}d?JOQ5Z5htc@n2`=w)Hc+GXrU^<@urM+os^VR z9{!8>2m*FES`P#J;uX%Hk1r{aeDhjFLe(0KJ70h0`}dzg$xfg*Ao2Ng1gA?SRE4lT zKoCkxm*0WAZxC#mG{rgR_g0)Ob>&3Mh;vZV(K|Y)bEtU?MV>k2<0n2n5BT|Z!?tZd zYNof}|5SMyo^E)YxmMkI&WShlo?rs{%A=7{QT>|PE7YY{pF482wcd`4hes9|DssEM zYx6r))siwxCGMvdB>@!dWnU%-ra0b8q91RLS3C>4xr56K9VhuR?F)I}4tWUh>YJV; zJ7h@DfzJGBpweWr4^DMv%HX!yMo$v;@a7KiTCfI0A;&{|P}8o(hO`EHj|~=#$;b6` z?Aj%yS561d+p?|#unQUm_UV+_)zs816u2Ml&sxuRBp!!JXQ4I?_ zJ>-a*Cg?J%rz_24@C&f%Yfx-P$bR7GaSYwW-w^3kx8PFf|{rU#L}gj-ZrCTjRaFs-dplx}GC?%V$@!l-(4^Wl?U9S=E# zN6z^A5Ixk%`YWeA(JgOzj1F3=MLE?4Li@swWJDsgK!*r1*G3rU%}VM2`|*cTn6k}^ z)yPEd-MgpnVu#Q^?GmnC`Sz{f$E%N8Ood&pBIW>+utsm!#>-3h+wCH82CJPS^9viC zpvtU+0jMCl+2S^o0U?<{M86^UddEA%4kw$ue;uTuX(rfxOw3%7`TLDVeqMQO@X?g> z$88(O(M1&V15N)=q9Z6o^`s*FWB!RYsQGqC3~cCr;07bpHUtq%SB2!YHtKntYZ zt(+^{OKSCkAI*C8TQhuVUM)vuX?wRQ&&znPBxCa=yE&+VDNaag5$EU{T9Hg=;ypH@xrPPwJ9gx|N?cI052;M`8Yh5#%r=g7zU$qzto0L_|P6_!%A~ zf@n2YI@iAd>M#R)9hz0gWYas})=uBIV~po|a?ugqn@CvqneyG`-2c&JcDD=pf&a4O$+FPmgdp&deG>{gn*Wwa`BUDLHvROdohgkN^U?V?4usSUi#y)eAhJ9z{$dK!(S&GXw%tAARw; zOa0xtwJtk5M%Ukqx7@It{@hZsu4Eo3UjK|Mx!PdhCEtjG>I4gE2Tp=mPs7Cja=o|U)w+Z|hvCbWz&k+XE?&R3ONw>pcbiXMMV$DpF3 z5AE)QAx|v!Jo_yYKWJXn1$?4jdOTG2rNF{T3JpB6@{6#;xIrQ8@w&7r(K4F}7X{x*6NOAwf6omW$IKHdKI6%m8Ic@~`fB&yG zDfDYzx{ls`Wu!1d3?E0%tl8uTE>)t7IDdK8*}>T7kV98K*H9DnVfY@B-;n z9fuyghcqL@k{OE%^I)QJ>Lii=NNmK4e0y>aBu^k2&ux-gEiF*I*l=g0IdWn&wB3{s z`372fWqFKl9ziV{*ZJlc-8!_e$=mE0-iCHZI7{t1f{L|(Z5Spy{F!NqS{ry+E}Ng0 zm$!A?3zrY1Dg8qPXD2WFCc=a9ynPN%9JY`#ANm(p3#Qm~Rkfk7JkCNv!4F(bII%kg z(OpZ8mWgl>}8A^lK)jc<%u&uZA!xq+vta&4{9~shB>i|o4EcRtTcwkLy4I1T#R<)?rj|b0$&G;lFBuG?7uAih}6ZVNk$ zCwnax9#`Y>vQUsNn~SO)*=vqOhCmulmB3E~G)Z9S6c5VFyAh&VY;JjPgCvT{ zbHU-dK%i4VWKE2-Q&Yrx?B>mz`r3_X7&L05R9C}I$F-2ui3GnSY0a}kDPImUBNABd z5TFb0h!?8r5;z3^sq=^V;vaG9UJrk8KkUFCqsgb6Tna->O?=p1oek>x{RVI@U-s6WLI`hZh8dG?v$ zfpUylBButiqk_#G*1^}_qBq+mTr@HJr2)<4hClN$Gh)l;&7Hr5$nyx@vgtx0*HBc) zJFy@C4olgG)v)~=R}_W$R#fp@fk&_f@$TMDa2&%3(~(t!0T)Vw{QTO}^Agej!Hc_! zNCj*fd5+q#Z2+*r7@Z9C0%8jgu8~)2z4|Nn=;&zKMkG&_bO+Fe(LRDC7<-MWA!*52>VOytzgb2_tCEA1OrK*MQ@BoqlbRZ(ilukcKRIn1W}u<@jigE z4B=Oo2d}4kBy+M_7vZ0Xg`$B}LKHP@LMQReBM&J?jzMOzoPdmrcn6XudbG4SIXP`% zW(Hu9CyVNtn6x2belUFpc(AnxK?_MIGf?k1zuUYFe3}YMO8#sPJv#}wfi~4z5HP!C za8P|F#c(MvVe)Dxx~s#831fz3%-Cil;2~P{+eKAymeQ_Y-w1=_2&7oD{>YM}N=v1e zbSy-?M*zCTc3{#li>UU%+yg-Z8ikWSti6J+aq=M^!Gmk zVzg)P-VZci7oRq=?=Rp%CUA`Q679|};uM7e3wE=1xAAC{L1YsvJ3wc$q>2-t8&`(e z8%uYvgsV2r0!8%3E^f`@-%3g~?uNu)k(|&m()4e_dUT!;SHUmZ*``_^I>t9(ur~p~ zvL7IaI!>*{k_60%5hW`HtEX#dC=DT@59JD+cT{a{v*2e-n(&_{wxgI(wyMXBKljpw2OUHNBX7X&ib_L*71$o zZp4p;Zed|D-dk>WT2?mkv-DIG(Qd`1XB)|%IU~4ytG`v_t)`;vW2&ud)!SB3tMBy0 zgC!bap1sLm%zWW}%>41zy#J8Cp2NU|6q3YBDx}wA2Eu>9K0$FrG@RFhY#TUbK=&@z zYEU#V=MRyj5ZkUFDo)~TsFtQZka4~(0M)o5u)*>vur$^hPjwpc9gh?G8zaiJN`--MKn?NknwwF-I~s?AJsT)+)xW7!*V}<71w557W}j zJW_FfeH!hmBx_5xzZLhuz=`9Oc=EbkT+gdUxaL{22P2H5Vvf-v zZBW{|y26=f)8*5rPb&a2Z4_rW)s_b9X66QP#(wb;HH8WD@^w>|`dq~-0XUJt50eey! zQ=LmpTbsOZ+x^-vrrC}I_a=c5ge%qhk`zKb#Gy3H~ZcEM?R{m zn@!0C*N2&oz*61ZsEAc6Va?#k+MC$~WSEq|#1?dPWaPjvm@QMZlM?bQt*jTp?CTdWP0 z{l{mHhIh!4?>|SZlc2wT?B}P@U3^UFO?|iK*L6(sHwNc}I(p>4@iqTxl2Ca~>L|G9 zhzzU++=uVj8RHd=5}m+DfLkqT4>$pVn*lUZs7AOgw>+-N^Yvmpw~zlCxwPAWW;Aic znWsvEvvk*3-8CYOkaSlP*wC)fC?@n+^KBT~MPg)K0J=gErOxxA{XAm`0v1w*3wxi) z78m6vdPM{|_ik|=Rn@24o?S~=mH7!ni<9f9>m1GdO zghY)OSMDwD@iZhj+6ghpBOP#+#Hk@}4sysspr1nli&U84nUB(?g<8v*xr_bGSK0ZD z*fMF?*HK7Nv-i(GF@n=Zd&sg~{+-7g+x)_+z2WFKBUBlNE_S`BBvDAQ81@L*Y`7@M z2((u300@J=l=oU^LU4Dxs(E+qimDUGIvYJ|vv=6mMqG>9>oJ{YorfR2EUIH9=h=^+ zKJEF%inU6qgn7?;;sPLJ(jhHFQ3k7L1&f`Hb$4LM1swZL-_M24?@Opp79k z02RG5Dm)Z@3Z_r4g_iox_zvCbCX6B#y;r_Ru8+qQNn{Ol)E{edYcs51@EN2*ac5e_ z;iGWu-6gjc(=?R|&%WD?(QU_Qd_<98V4Y}&_VWZQHcWdfzN=2rOg=d&h2}^+ku(k8Dm8`bo=&gjl_5->{Sb9 zh*z`Z`L|`C->bixIdK>4bM#ucStoKG`9EHL2?R!N;GC@NWed9l*b(yE1&>TKMO=Md zUjBsTs=2wf*3w0j+eH-~ft&ToEv_hWbV4j}LA94f&EYvmot^r*8y5+G;uwl!EkpmE zb3K22=;(+o`PI{3-|u zkuRSg|4^ZIqxa1X?k~T!DdxAx-;H1+N1(YnnpZXZijOT`#?t=?a@G&TZrF5TZdJb< z@BGehbe?_;4p$E%8ff z`ySZUdrNJ)UnIvpEe-;~K4FuJM^xF}dLu%3P~-)z|0Z1IIs~opQ6EIPE|N_^PTk|nez@`uh@vup~X;rZ47q7nFL$(C+(NeI1`;FSK_ z%2z74bca7~G%&ZOj+Z;f=Ii5Q%iLIDN;eUZHMd2{c0u&gCIVb&Y zR{yQL@s4g@BWBwpeipW-E~kI!dir*@+%PjlvQ*)U@%LNS$=a^Ew9|RO9Cs&@A}O=G zEiUiZn0sQv{e^vhfNpX_!ehIx#=PU_EO_5|=lCzh8P5Q=WSWT!+P0n*Ka941rRZ7J z3zHB^Rlo_AEJ|rK+hY{KWebaa77SNt)~;2yw$4BpI{!1JjiErSmH*Zu^0t3o;ps;8 z3BK8KlH0#Au>MHT?E%k1MU_UH7;M*g%i~uo=djWIIf3rHH_F_^r&u#t3(d7@HkqpU zpO^O&z|weLkAnh!3@Ev+E~{9KmQY%w&KIG9ndp4|$Z*>FS0WFZ-{DMCCT~QQ~PPXFQWj3K;}UMddAx zID4wh@|9M_U@@Q~*0KE|L8bq1)h%-donN$Ge&4>*hP#c2yUW_R{ zhAycHl(->B9o2}RITHEq0e`%mo6*Rww{FbsjiR?-1GMy&(na2({zqG$1)s>BgXgH% z|6Y1Bf8V(70g^?eZ^fnEcBq1}S2fZvdxScT%A7cPvf+|Vydf~3Nn3KOB{MycBiOEy zbgx-DZ~wlBn`bqaT}8zy)Hn6uj572p)}gxT1A#(IL!)r~_-#xBj38MfexTfjIAM`n zuyNN(1K^8*oI(kI^G5wvq5<&0Y&r72i76LO=V0xx_}+NBSbcF5{rxyeZ&G^`-MgGW z>YZq!xtFg1&Gq=vqbNMsK9u|_ke%58Vy*;Qy?_5F5wo{`J4wC@WiRC3^G!}`;{O4J zY!-+&bvwCNSgdT<*h;&;T>!?(mPYzB&33_%Eti^^t70>635_C13v`*E@Su$gZ0%1X6+i-mw^J}g z0pzc8{=C6Ymf<>eeeFkq0elJEU;F|CVt+aZd0YGn^%hduW2F3U!+kD-ZT-(B;9uGm zQS=LE5e^{Y7&Y+%Zte%>y0|8Y;(*O*SZWR=?_*B*_Dd7PLC}nl?qAfiDYII>d^xem zBWsk*+$b$AtxHtpg*BfFt_aL8$qxyD;+Ritxda7vm(l(s;~~IQasLh@Pbnhamo#S- zK=*4$3X-j**SRh2hd-C+#%%k0$|5cu^;Ar1HJm&#AhHfHix&KV{iEC4FX5@nk*uzw z?=}M1vpoU=N*x^?MtJz3jgyh7VnMcbO_Ou$-aSR&GzbUi$LX^!fWFUk2Q@Q!5+ePV4bzq1ckilXhh7nl$sbJB4}A-l+J@-12CqU-Z=ZH;+jC8yLCJe`qm+ z{kc%EMstuvGs8>dK-If<37?NZpf7xj&L0Lp3Ik2;dygE^B(x*goj44th=+t)zCzfv zIi3j1>udmY*Agev48u2pky`*~0t}r23>tvmT_Ub)njmp!tKwI_wV5%IJE>*fS61y` zzRpL7MY`ve1ZQNOcyU9_WUgph@?6)R>7shuyU~d@jf+Y5-wOmG15D|5?Gz|8|7arN z26NN^te>bBkt=`fngDE91Hp@R<-TJ6@9%&SW0_SJP7;+L>1HG12B1aO-(Ig-oUv2n zbi*Tc(i1Vy2;*7^NDHCb;q2`jyw=ju)sgWgfIp;QcleN#Fyi=CB}lNbu-+Glg90M1 z+QejBpt>Ku29zght2_4n$8{?GR3 z`FgmcIoFB|1Asu3Uj{+u6Y=;4noJ7d@@cH$ojZ4uwS(AG>$G;;UT&+6PJrcz`zyS( z-x|6CS0}I!ad{`+;b2ldbWY(UAagE~=zc&EyT_(gRb49vKx5*ET%vm*vIc}; z8^ z6HocFX%LAo<_!Q0Gk~zs->a1CHfi%|r|)QNPE>oi_uHu!wHH<=^d!-p@t;>+yuXwu zq%H%PuYh-{CwiNR!&PAtX=q$NgL%jjHc;_pcV|KnzUMVj1OSI91PA~mBqKGij|U2J zE$j$CbTmV{J9y~rCmhD)5Aj@6+Ia)m?AwCYqtxy(6$v^oKm2LX@ANv{`b0dKFGNY&w1dbX)nSq2ljmQc+{jCm@ z38#c=JAmfKIASXaA&s2>9|e;Zyz~jF3pfU`Nek2%s9E?mG%{x66vMa*HAEwdqq@q7 zBWMmHT|4QFTOnz3H{%S+O+2P*AO81aFL^1unhA(I=T@5yPr<%mfFRSvU<;);7@S!j z>{t`(F3(^#&S9H#=tHrlJs`40f(vtUx-?0oQ@-0ltiB0VOT-g680EWN08E_p>%bO^ z$y&K(vCh-Z+@LCr<0D~_xKx@d8u75sy@JOez3$@Z7ENxb{u0bv(*Z(YR!>@VTQ z;(ESw#~5h|5aDY_pp<4lY#FVM;RGgMp1(3l>4IU7d?(Jz#q%m&b^FO9-v>6i(Z2g= zId`qr^TVGU{?Gb$^ZW>1nBmX!Zu~OX$)6rD9fW~iG?u&G0-@4f=bf;=RHwwT5e2i> zu3h_=^}wop$eC$adlCa$gVPP5nwKOZ^-2evUk-Y|@i_Z)yF(v+yvn=vvUy#fg23w? zTlclworWteXmia5_HEl@p(U>|Y{L?h3$gqE6YR)<$gBUO*x4oc0tdd56m5dgdr|>^7NpF%i z)aKaxN%3f$3B#8LWMKe|*z4|HKTbU9i-Z`u3Ie#Uu1;X62E>_7@8>s+aKR#+7t~i4 zjvvuOK8^td0Zp#phas7j!4Qx6Zd)I8)%6iceL1krn5vP9385^27(Z-NaS#VMm}#!! zT2#$gfTbi5Kfu+0hbSs$W6zxmDH@?viB3MS0#?mhKMGkr>6jv~g*i9$PUQAwDID3- zks1r#KT8P5qUN$z1O#r69}=K&nC~isG&eOqBPT~@!&rm%f1&dr8d^5u2~M2-ft1_e z?C*ppbPGu+7xP~c^Gmr_GACO0cu^l9k!Z94j8+du3z+u_EbdKj8I;J`a`86DTmEaW zN_xYA-jbP`nwm^xeJ5ExM#p0EZ298Q23SsUnd7%XgD#LdLTaduS1lEzzH~XgEpy z)el*R{L`*4_Bk0~fu?%@Xpre&dYe(hF}}Lj<}!O0&2>Il6-SAuE-A$H1GQqoVh~UM zrYw^nME@8FkMf*TQll{gjYxEf*q#vmo#o!jr%&JIOaLuQ3>{GVqct;F;Mf*i898Tg zC~(Tm&(HH!2O&U`A_)#lEw&UB0Q6}p#OBV8_f^Q{03$vIk#=a00f7TKO4IzLxYQzv2R61w`9yE_@{B6hS_$G`U5B(huL39Jkf z0HMiSwlN4tF)BmS1mqu$K06}AgU^a0Y8P_OqyA><#;mYC930aAp!gG%#-KfI{9+1q zA{KKpmO0}rncVUG`F@$tXiFuW*sizjPa7lbPu=~hP(U&cF{mXu2B$YG=f{`d8Tdapj{%Wuzk`E=TIow21hw=CW^OUyvSPU9G+o3P8MVg#6A-cO4p|%kdBhaiv6Gb^@ zf!kg0wUw6)O@fZ)Ee$gUS`zl!jzz&QINw6k^4;zjAG@Dl6d_CShk>O0B6P!6%NS#2-5ferE?(L1}r%N zC9wHR^xGxf73{) ziGR>Jd&n_=Ww`WbVWBFCwSQ35zbhqNR}64X?G!dqK;ArRjAwu7cpOis{jocPs}4gJ zKt$sGAcX_rT#6M+PHkcdJyY#EA4RPAaiS4n4#wJBo;Y=iSmgILWhOZ}d4;aXh3$SJ zX!r79_`1)am=lDB0gwW2@JdhskUZi+Dvf_|9%D`)q%2sY>4alL#g$Z-j>yyxabNz% zskhLlLTiRF&o85dXgPthlck&gV%8brJ`E^}Q-(A>WA#FJz;1|{XrSm(5-8$wNis_+ zaAfu;xgOw@!pzEM8pB{<0ZX@jkaC67H03`hk~T&U%# zzQ}R>z4cT=q+)C;bX@FFB3U}=FyK8}0WqHt1`3c&8-M^wh;6E%j*q*2dnIJ5$52b) zd)2&sxCE$@`053Ad3hhw<>XIJn0RFFcpWWeWJ&Vn^>7ezD_1ZdC)b)ylWRyuaVDr9kUe$G7(hKzYl8*hn-=mdi5$1)Qk5@=Y z71OxuP;4On`~!e1uQ?ZCMF;|JEo@RUK9>)ZUQp&KkwX{Yoe#T2>VpS#1PPi!OAiL5 zR#|pm$L%2%9nsN1F;2W^$-&O?4G=lPnY(GvLc_b z_?keQ`Jt@pFc|+W>SrH6F4B#`&yU(NxjhPDKhis}slaCu{r>wKl0u7x3BI6qGT|#E zWsog2O}~0HGf=5sphdd_gI)yH7Bh&l{w1~g-(LlYpNL3J7TbZU>gvhMLgc?_zWXSv zjBn>3y>1uV=+tkZI5FE|yXT72bxz+-uP*+Q1&-m!PJZ4NL(Kx80)HX>Ybp*d#qImM zE$%gTCN$QZzkY!^X>s454Ee9ZyzZ$j0{54!l&00Ic_v=3k@?(Ir?UOGD+Ql9p#|^> zido!U{69C#?^k0C!))%o_+_dBnHlZ<+?div(bDDk7NSLFFFE%Z51s7VE;sh}(Zqdf z*7wTlVc&=B>0MHG1($=CJIPp@HdSw;*+$&w`mfd1EdTYKe2o|wce^8meDNux3GEHxO21J;%092-Emx7El=Q( zvyyRJ%*^foJiFn$xF65HW~Y^7_QJ0lQc}$9TcU58P>iOlLY|=>-lvL%(|%*wvSspm zfTs9W2|i?YFz6$~JNDUDa6GZi^t=IBAlY(;bzamvc$i(d6YQxXubjI3jgoq6V@zt= zRY?whUahZ=!aV$9$1XVDmp*9j{hv#S&%)>R)l$sT>VyVv-5Nh$>|#WD5fke)p-Qic zC!z2XhwqT!ugS?trQP`SVImZSm=Y8lvNV}iK57FMFT0HR>!)b!-qW7S=?|AOh_L+6 zt#&g?ki8p`Gt!dJJMmpc^dKHBnM`|@BKCQrdcpiqb-g&Vj7d=tEdHm+mMAu7qkOZ3~) z=U#b-;kQ_|6+es_wnXnPyOX4KDZJtbt~;I{XJrkEpNPBXxXy)++aAX64BrCXswV~s z+x-B@LH=t8s5F&inFtj*+WA&f_<*mRd@wv!u=pvsq60x6LHJVj9r+Jx9sc?1uYrqt zR_m`*)H?HRlG3ODlu+khe!X6P2tW`wX~q5XQrNSCl2wmSFdk<+h|D0rq=dO}F;gZ# zFud-+-dD-!1&jN!{P1v%TmmlP)xHM&)ncD31Mf1onTW+!(WU=HHBm8T+G13|;%<>P z*z$Luq%{BeO{%^XU{YlKb^2-aetzMSad~w@m$~@ztgLpwUr>PBkHXPr*6PYC^W0(% zt0#`tdV{`!b|yE1TjMSd>5S;i-6{)i&G#R=@Xi6M_{W82FBcn~uIXj=885atEnR~f zZPEbW8adDP>UF@x`;IQ%2e6VJ&?^fi_Zxtw2S&I>@58}g6=G<&{c)u$immoJ2u}+&WTPH+XdMDexBYolCkjZ zy~C)hIoI+P^d1P6rI$?=&wtqWzEV~lAtR$Y?=?lg{_y|#T%CZl5a0tpP|qVO%#8n{ z@NPB8d1t8U zNMh!6)|$;cPR{w^JFt1wl>w=|UpyD^^OCCLH>`7SHR*H-yrxolyzA2^o)_7+F2m7y zXa!pSjif*oWF*oP9<;xRt=h)JK`_rNFTXy9&lR=>I#X|vpo&+bgP(3|p3TX-uR7ZA zZ6*OP<_BwAVg9^XBVWqdvnfj3mwrz2EKiZz9yCtg4=8ywQ*}PV?JjSj)@o+Ic6n#( ziO1QQ5Px`!e<0gTAq}l<_zMy0#`UTt-8a*#)d}3q!x$v??&`}MKgE~cbmEPFM_H0} z3r?Vq7qhFn(!0cLRYY0SuS}VrD4f0;VD#r^yS=9w+;Z&hG9mC_nnZJA9L54-y?_wV z;o8`;h*&8~6{}9!tvdDXFt+^Dmj$_?q9GbO9lrD)f}#Vgd*9e+sa z#1s95&!)dl$@-SLa+RS|wrPy$U+cEkALs9LZcR~^o@1Enjv*?Lr&vZD`;-^gPK-T0 z_Pk^v#G{%KV{i%CcnBs~37^>4;d6U2NRBh;Dz2X2yA7&_CmMRYwx+y;NB&k9MnmVWzRak3ql3fi zK=+?4!Oh=}MZcZoKS~9+$jxtk{`ER`zkG3TnyGz6!R+B5A?RSNlyrwM`OW1TRCO%l z>=}z6A=#-CU`mJB&&w(xV*l-SLu{;C9&cF)Xt8OZ?V|SIj>q&9G!92fH|Ib9Jef8B zytH;;?8}S(r&rX>oI+ELjf_)SG{Bt=ILVHli9Poic|o>Zf(^1>{OWs%HQ-|C|EH5| z&(56@C#C6jL%wMM$q^I}d<)?ZgM-f`GB19VAUC3Qigk&k|9rZOb4=C3{HF-nwkRzH zg#hVA5f(x=v3mjX_n74}ch&Vn5LR74PvW%@`o4m(xR(Him>-(jRpE-XYhJgtEie&H zmgI>LG?T$28a*yTzYfn!62OMJcmb{PnV7bK5|wucj3K{3QOei9P~AFLe#o+IvS`7y z-~F`Y%M62PyJ&h3%Fn?Sa-t$-O-TmluB>3luDLqsF0$u&sfMpWm3WSc%8&5%4LdI8 zFi7!~-sgyCigEUz*pkEWLQ2BClj~6nTX1BmrCMv|t7SeJ;%;5rHbm7^9eq-8hA$)6 zpt_YhC_=_)>YC@lV5ZTz<#hMI|Mt{TeEj6#*zE73*u#FJi#La=G|Q#e|4f`xXnBFd zY*s<~%TB{HnQD&~#_U`hYZ*5_tm_u%qAYZ}ehOIHWS2^0WBfIXoN{}`D_^|cPdDAM z-j$oMD{uDG9XP8zKf=DC_zxFX(5lwXZ?owW{%&BGktY!R46`h@*g%y#Rnzn=_55Bh!)GfTU9=icG)`=u z6ebqkG_Y=gwr8 z`-IuMkq(!-7&{hR~s&M#T}Vhz1aD3(CyXEa&j z#N16NFY@zQGzQ%Ls=H5oADNulcHfM%wU})|dcJYtY@qVF-}&)!QY`D&4^`q1zrA)X z{<1JMIy~%Bu^|6$&vav_&fCqMVZJkzc*o1X$3}soLTC1_2T6^&;P|3V}`p$mc*6Y{JDz9et%^r6BH5N9@mS2>? z+2_qtRI#*g!^N^UxF0ktUt22}qm*zrwLj8OV{YC_oZl;w)3(Yxl6xZU(Rqnx=g~IT ze{`nS>v-oqcP`de6&Nrw0(_OrCa%ldzk9gkZ8LKyhkCnWfW20+OZ?CYQ+f>(6I$}qzrfGZT z(#4PWzlW}Z&C1z0DXqKNakymiORtKo+SP0Nde`F8mI47)yS!?B>|wE3!x=DA*C|gR zW63hPA8R-WECzNG&GJ4)*HRJ zkm4}7vLh$!`QPMr2d9U0#C)dy1*N^)R(wsx%X99CopWFzoun1#^DP^jKA` zH#1QFDTlpS$`g0KA@IqSiG~!(KgWBCUS6v3+0Qx`FRry-!1fy*eer$lMvdUv<80Mi zc>-iFGoHM1|CvMm6!UggR_iaDWNgvkc4M4dyoISbj}t4E`_;7T=RD5WA+;$xAM7tX z?;smn^!wxH?K?e}u1_dS+gP7sdwtUvQ!S<~CwA&D*{;uh;#YGNV%Jo@t2Y{J9CbD_B6W=nfxm< zn|!9qb=R=7Ywu_FO-R*tRAISwb#=v88jxp%t>bdBHsdvC*<$!8F&L}$?x?3^uSn_m z=r}pk_ukw@_vWACQBsmQpYBW6_8C^jvi;FY38y0z;w7?U4gEh}-3=SsLm$Gqz2Xyj zI5tt3JKE+$ic_eWn)*R#ziQ259cWz>%l*1sVs1Q5dX2z?n7Aw7eiyJh3pNk^+jch( zJ15WZ2)EIdXFuF{d#hd~$FA^i_~o#)p)9)MSdM*~q2DQ@^@kb!ByqO3PiMVRsE_`I z7X<~)<4#_Yr?CYSx#XF6cMvQ5l^}5mC_-r?V4^#~W?1 zw3YY*V^nx##KRa6vUq>K+<)b_&}!<{M_uD^Na!-2tq!X0-W^t5F<4kDA{?)jaQyt5 zQcI_twn_2N*Z5Z|tohbc>)WDX7g)^fZi1eZun$jaBPuU<*j?Y3{QAC65l`sv?J zZVVNbPawbc_>q4;>?pgM*<`2gX}osrGS#IOU0}ZUfc3XCPt-|>d4%8$kXEDczI$r_ z*`yY|@$*l1qa*&TmBp453B?kTp^s{(e!CB9+j|6Nfo<$Aq)5Njnew3tYQ zz09AmuklQ6GJCl3vwu8wUwv+4V zk<2vbeHqTYUL4P2h0ZeDiPy5RvTiVE-1tOahA(h4*5=SEn4D|!-PdKj{Om`G?=@yI zPeEJnlHtSR%|pCzcW?-Vitnnp(~;8Z+r!#CCu5)+Fupj9VBqGe6}Ng8<3%c6iJMSp z1v*Ep>DjrxK5r2>SJOE0Ttmm|&dW#>;S?zvE~<`ZQ+g>(|G`)B-ke7rk#6jMFrj5S zxzE~c$Cq{;P2bw3WATapz104Pw$}^>m`g`Kna|dD%oR-@IO(Tr;K3yLv|#v_)AiIl zSqt2x4CR#8vs-be@ky&uwHj?7S(3L==ZgbPE%6 zb&>#=!PG$8^hL!VIz7hho##RxMzy%nN+;t8UkpuoBrbYp1yv-VL*kSo$?Sy@*b-Dk zEk5NJ6tMOSzD!J1;tA->?$qYT=Tcn~d0g2g$ouWkK+l6@6G-CIf9b$IKE zhZx-Enu_y8A(VRL#nk(@cjq16PukB`ox9=~wc*l)Zsk9H5XFtXw){&%#blfnoC z|LSh)nc%|tl<`qvAs=29j>83^yk27~)3rq=CST+kXOCTP$-;za-v?kwQ>(;I73v28=()}hy%>AW7B2J#HtZh@*9P%=wvTt;%Jk%zeD z7-~j{?+Nx0qA$Cf_xQX~@Yv;Nv0LuHxy#!pud2Fd3oVzAmDig{6`p_+aphtNSSHdy zcuq_Hx;GVc@xo^P`BiP5>};)pABUWUO1w4*&)E(bTsz+;MnI=~k^M9GQt2!t6p&EL z#wg9%HoG(2EwaK9$7rwqKN)Km5eU=`#-I<~tq75BJ-KjM*2aB|ffJq5o!J+f}V^InNUD zbJm@`*HZAxQiR{cbx(_l*|@69(wTjO$63WKsANL3hE&re; zh#T?lbhyFCw~~X~`xSL}=}uj&TsK_tyOLychV|O{2ax`aPGq;zrbiCm>U%$ofAz;9 z5T3XJ2^ME!;z_O5dFEn)Ken9=FZ-M`_4ZasV zD}G1tZTDED;j_C(28}K~yZUTFM$ULP3o~6RGHBa#Pj(gaAyf0tAaOqXjnGNSX=9DSFT+%LOJ00tQ zC}YOjof+TGv+G*rz1?Ihnbu@uY2ldMBJ%Lj+Fc{U>o8?EGe|OY%iI}j_Ctw@vI#1i z9!^zP8=hKRm1iqhP#)%h9kE7nd|k2s$! z8kH644)?VkdQaWoMyubZNTmIzIkU%Ig4eHGN*qZ5N-AtO4s9|3|)vw_i9--iQ@5&~;QJNlgCm zaiUg;Tzmq>kHoBS)oEkY+huT@p; zLJyDV#`xX_k0^^l-@7 zl-G3pdwQjmww!q1WjSZ7xq@LOlc!w1EhUAkG)KgrwJ=rs6wPJp+`0L@IiLKw`LX=< zR3+wyN7tRDCGgUNhq;#|46tGE-i>x0cK1sh0eM=(W4a7swQ1z@jY!J*G+(>UZNuT4 zPm1K(l-OK65)+u3TX>|wbfIPCjiMUksW?jR_zoM|c=P$K-c5xId2{;)VwLTla`yfj z(#jujC~O;OtIk;vtM6|MEH~+Ve@WBSyCXtDWMne@ z{9Z&^!?F~6ihj{bqi)ah1RQlBDbMycW`sBwiSPcMTkqIm`7G~`HeMeteZ|rRD@Z~Q@Xh5>P;^S z7tZfplkbs6X=CIs?$&laX&SQ6f16qlXYY2JTLNbggS*D29X^%oUU)*xVlQ84xVNEf z|80>^VJ=(=o3Aj^|7Z0tIE|%s=~*2eo;&G53cr!&Qo1Y?IpiMoSMC6~kWldLmjY-M zY1nkH@nFKt+;qOTS$5)y{*`U=6lgzeccvd`FY!1&UfS>Br+rMTP+Z4f{n>J=9oOZ< zbuBFgEExPkB;9py*WSAEIQc`>`Gy@IuF4lr-%YUAxkmP2LLv z#Hl~#s6%fZrs%-YhkAhKmR0yC=Bwl^FWfn9a17)wl8~q zir*xzGrp0^^CC5E_Wd9iX=WOo&gwME>kkd7qNXj|o0|>@NA7wpV_Ga`$}rNRrucJQ ztZv52h%Qd#)mE3)k_8#F*^L8j8rS@tI|rnG=lA_Gd-Z(Y*`kQQJ2TTnY@^n+;MqBonZ3(I$9fk4uy^jABa^08m+LNm%U1QU{8BLbUfd4#oi42?ylgSAaZ5 zc6LWzNo(2D2BHx_A6*bfD>p!k=6QEo#SnD;<+szUyTx$mTaT_4haR zPm0rRju11P6wR2uxb25yY-7N|6TfYi8`aH-8!@=`ZS(BQ&`Qfo5o|K8SIC~tE98q} z>@jYfh#Oh(*2y;AoF&iuN>u7$j-8Idovqcs$C`3?tU&_6uT#K{zrQq!7tJ?Zyp6pa-fI{koqKA*1VYHuy3>y?zel+ zI!^})jD4GQ_9b}=jlcu9dt+mxAM9=wr;bz$FKCD#YFyD~wuO{;o<5P>sqYLnKGfvn zdZK?qLDkAn>iBo4_k5#KcQfav(4LxF##o@O?)h_z`I>LHZ)CrlYxzf(sz>XSZh&G* zB-Q-hs{*4~-=XE4hL;y0okafP9~XWy=}+NQl88*l^VHL|FQ-x>l!aSz_GK-jIxSu$ zlgk@eIpJz{r$Y#b3ay89Eyw<<$(Y2%0(hx$ZM%!okFE^1zmfk`eXKl~G|v_9`aBhd zo_TV-Z&m1pxuBAK0pMZ*>l=!<_J~JGV=e8^7@aOKcxL-;K3>gSW$C2)wSV{LWmHiy zLf=2xhfz$@l0qjfA3j|Bkc#R-sBX>h`yO_YVRg0#^7#Pih)?5_5E>ky+m+rXHuLgNsn__$(d>5upvD0=UTM~ldbKV) zzB#GzStkG`hl(fbB`28zxFQKx_Iwf3oPGVu6Hq<(`x51vlX8tzW)AT!Kmf%wX2ty6$bq~v4?7eXDqSc)E-b(+jF8vxCEy1%%#nmAmY83Ak`X=wz;WB z*tBg%2*I7zC!)}C`rC>4det3+lApo=Hqy=0j1J)ncv4_z!S zM_h6al@DIxr~Km+o2*^av~~xtqmvJR_4Qh=W~#)Yy~SlcKWkg^BR>?k8~>cT?Krim ztb58_#|J6*>)f?K_vJ`2c&vI2Z$_4#6}+D~Vhuf8BO)f+HB?nKDlt*#;STl7R$@0> zO}d_&PTHXe7+Yylr@m(e)$6qkzJ-n!SIcXE0mRs;x~Zs8<19k%;|aMyry)IkQ~B24 z!^cn;u1F?*>Lu|;cJu$fOJUqSr|D~`^<3b3mPsR5M3gZ3$N2rza|;&q2f4j~dUhOl zd$pggT1f0BL#<%Yk6nj5R{uY;z5^WV{(buv86m4OB9axd_g+ba6e1B(cA42LBPu&( zuOuTovt+MiWRGmJL$cRzyR08p1i~Droubf{+d4wk zIVBABea^eD+-IT;KVy-!x&=I^>sX~{G|Ovc&EJoG1bT>J)Y1g%$KAnL$u&?`!DQo?YC*2ShNP!hR7BLE z?%=>60r0TMGp;2RM{GT&#_Ch{^t4he1T~-X%`!D3AkutEu>Tkf^5Z}*Aoz=bf-tm2*tWg8s(K&u0 z@5-7R$%SS}{93X-!_==ZX)UrJ$>v*s9`(Vhu~>E{-6)qVJx#>ut1EC79nU(LQjtLz zet1p+A(Rn2>76YbztLUuE`h`|t@Xz>rxPQy$v>dejVB_WRCd58Kn<@~Bi9Zg{=Hm9 zYa~vwrVM+8zFVzqqLcRh!J9Ew$8+ba<#WPs2cQ4^?M^Hib6YhulOtke1d_S=&e!d$ zy5CLQgOk+WPY=~T`#5^4q*oZdA9D%T!#*de_>#S-w?rsGK zq}s?*ioeP_j^}f#fS-o61|%oXUe%au=c$ME%3H8)v$md%41=}!v}qk}tWwRZaZ^)M zM|XOzf@MDYK|h&-eH(f&xvMtxsszJkxz4@(1LLyo7|>J7P~P+vxvkJyhWF=QiFGT9 z*S0)Nmx1iFc-Snh_E85RxAV}U`r!oqbk#v?m7iwYxa4+kUfS-<`^_(lioR~Gt$6K~ z&X`Ici5_Q$0y~ij*lr)KhyNhOKTcJ9`URXXcc`&%W@=7Dqg!?dl5|Gi;Fi_!X(*}} zP_-+M-~T~>@FU1RCcvm?4BNM)y!}Um8^C`=v znuMfg$bK`9zaYJHdlTbzM;N|<&;=zMS4fGf> z81WaKl&a2#;(X!P2X?Ze5`CprSGqGbJBFV?qIe-k@1pjZ1Q=L1gD{qr7jh8G9h{JW z%ElB*RANGJ^P26RS;Di9~2^!ruTK5H*Lw*{(Ej!6IKGO5H z>M}pK=UD#$NuQ(WP$h-U)xaXiYwvvHs7VY}cV!3wZ;J9TBjUfIrk3cuKRzyFHOwN5 z&bKP1C5!V>1;}pslji(?;n*2T>AdTgjte44a~>y91Ie3YFXxVS7AaqC%r3PSA_>m- zx8_AYG!k$^4x?oKnbHwfkZdbLvE4lon!OQeJ^AyCM9~7bjN?kMbkyETB^|ur=fNoH zw~%qiDXGF-VX(+g!zH zk8Uj*icB5E%ivIt_tx*Yxcm~Y(dXWojq|!fAGheU{G=0|{ig^{IXR2^8Eica!H9@6 z`d$3EFxin`mYi4tHFv+8Od}f@tXt`F8vz_yY*LV#I)@H>wk}A7_3D+H6x}60w&>nJ zQnIqlV*3h;yPny6pWFC-mhO0!Pk$*@h)mn zNMne`i2wJ7pkA?jC<@>=P4BI#@c}0E7F_)R*T+Og0TcPhm~~y`I6WZ;+x0I!VRHW; zL3aFZs3aPB0>)d?&33=-hp5?KOme7l{nx5C7QGRmQ%gC zRTJtUx;v+4xOYAcgP7R_a0zLA@hc^M3BT@CH8xARm04C&TV$>0pr6;Dx>y0c{mljR z4$4fg6=Xn~Z@9$rj_j=Mhe~nWCl82!aTe=N#g0~Jl0>I?gankT7SaoFF5QDPXXVFqFg7)S76Zi&# zxaSnotj%hI&0|#~6$Ml&x&_=WplgG@>D6>FRR3_R5K4NoOP}doMz>qQ zvq^!xymviV5dl?8i@bA!A9CWLZpp^xX4UeIr8!}naU2GkJ5@ab$Hnf3hVkA~lScni zd;lHm!m@&@gr7d0=D5F~iXgE@?nit;YcxM9rm=a5zHlh2hiZ53X=%-SiZcRzmdb@i zR&g{s{73xlAw!r4^G+q2I~7s$zWCydoiWeFxiGmr$QJL8LNB~bBBoTJz9TAzT74K$ z9#2=~>XwCI`PbqZ@#b-dhc}=6#4m%G6?tBXbqf&iGIav1`Pvu^neJSi>rf_utULDn zH=Z|%5n$>>rT%Uiydp<}q|~{J3l`orx>{ZH>7Bcdw4eGWaKuG0buWgWZX`)9yT~1G z`gfsEKPuUW%|E-0JjDDm96M5dv0*097z46uSJG^It@(zs`)QI$x zV}7}JV4#G=mzC@~xm$OBT;mCmI>m((DJ4ulcZiE7-Zpm$p!i5XY5absW_{tupBYeyr^1{=j9kpiR#yQ!?HIwq z9F6Pg!3lKbeEd_Qq?0<|O-!Jyugya6e-;gj@V@4S5E(m|P`Thad66rH8A*7scw69x zB7|Ze5CHww@wtQnMnuV}&NhUc$0fIh4)J{z>3UA6Mqn{~n3AU8n$!>wW|FsFZX?)`64FQv(pNLW zz0SKg81|@{5WApb(I1?m!x`d08K78VEj*GK&y<3;D%JUVF$o(Izjphy@uP^|LPTj$ z=`p;u9aSz0cKO6de9W52+>($Wdc-KE*bUnZw9K;GzqOaS>WdbF6;BW&j7}G=R-Vkc zl_@Zij?BP@r>R@*7^lsmqmG)EJnP|efv0bhDxG)y=qtj6zVGFB(VQ8I+uId?|nDC%p zr@DbeOqn!HZFTC;<`>qG`!d*(+zDX@sd$3a_@kmhEN${9Ie8Si8fV5V#9Oki5RCfK_OyTR+hb^<~ zDg6r{IDGrZu><=5TiEM_O~~b_zx6qnFgBgIEW+8+BVp3j2K}L^-BLc`S@S(lmg&$! zsC_B~#yPJ5zU4hV9sDvz%-=E0chINQ()#@P4tvxC!TXSE12PnH{19GgoVj}DDw%3G zWXwv5AK^^gdA{WltTI$@?>9UCTYY!52`ctj+B@)Wwi%)mL(1_|+l^#-XzzupGe^5k z_}Gh@eEUcRn89Z#%{j*(gW|xTSQPFxt=>e|eeoKfvhTX=XHL;!Q&r>^g>gMtoO>tk zv&8CwZq@SukYG)}$h<7hE>@|AerZEzXZ;zkqPtPRT%}nAj5W50K=Su%qZ--|p0PQ; zoif|;hR^zW{pBwH;qgs3mu%y(uMosph8qiR0z$&7r)Vc}IC?EOQDV;u)k}nnxCc&N z*(%|jyl%)FDny|_&pRJUMEtKqGpAGO(v4K3e!D}72FZw9hmx>m;}YwnGDH{34r(2d z0EyCTjWYZ>wd>jE6DaqH3aM*qF@qDj3;J8jsmFIf;kuSqzuHNN^PsJq8$ic{*hk&QFoQe-#`Uy- zdXb5%>s`;k@92vYs?yk$D=p=y?Bj2AOYpwULf6$gP~d1ZZu-Nm^Q31${ZIe`a_4$|F7qga<}XXGZ)$O0%Jq*mB8%oq1731q*3GLmt0( z`oaNFYrEUu@StGoMDhLYT1q%LP>HL$9u4VYyK>GQcLOlFP-t`Wm*^F4ZpcDIz3XDl zOXc^I71o`y0neWwJ|mhhqCAm<-LlD(EOTQQ(alD=FaGtRY~yUr@=`5D!gipLUnT{u z0(B>sVIr_G9A(5CxkjsSyg0b3{f_;HK2SYqAE|KR`N@j5XG7N^0Y|WT$Js#VO`LZg9>h(iAq>>?zaTvUc zoy#;zBd&>Da(KKaCLPK59~HB^72j3reU*M}4f~N2WyIS4n?g;>S^66XopR3|p0yL* znc?uE6`B`PiK;v9W;4^*Ep|piW1xTL5ZjT|7g|pt8;5{#4^! zv-dfGfP14&_2`k~<9eUZnJ|V*%gnCtVBI;RZfjn}{MxL4YkU^f3%*OKSLwL;COUo4mg{zb&TAm4fnXUWT=vz|d3P(5=u?1NV-jPt z_oF1weNW7da{eyQhY+|0k76GUBl+j2)xGtUM#n4J3Y^4;Wnm!WvwP*Ta6>Ts>|epB z@GX(D&wuyoZxi%j5XnP9+;9neKgWcZ)8~ltt@WU`XxgrP_mC!O3Nkb+G!FQ_dO3^} z-%G`$_RI5Zk!u9b-KortHSFM3g9OvMLeEKF;{N+1u~FS?ajmlAUz86E6gmP8tgTBzG1)5fgxuH(Z; zjq6k_jU;N)kjV1O#W-t}l7wov+E2><2D4#Dlfc`R6tl3YkKi;RO%h{!)ap&2FJU7G zmJ!KX=WdnmC8I~3qQVrQ0BSbsP#6KfxXXYjkZ9@u^AOeJpPq`E-SZQC{8Nn2Piw`N zK6|doqgE7mWjp^Um&kZNQ>UsFIjZC}g`E9oIAsHxPM?;tRB5zCzqgDX2E-vL&6@-j zF2<(UHZMr53ihcR!dfFwAU3+BIDPL%_p6N9-*+v~2ANhG^Cnt_n7Jb<$|cjK%U9`4RX%i5}Y&1(~bnfz&$tt%{|KfcfTjP1(xeQY>99K zKiwxS8IkzpDY;~P@?echUFcfj`;NA2`f_^6ISeA}!&5@PmA8lv8pt7*BLk;$2!lxa zb^bNBFa$a1KdFGpB2c2PA(&9^&!wQi*-B%>Jo3K7Ag}_b{`6>84U4L`tswi0n zDM9Jzl$036r{MErCHL!(_A-Asi+vrDLjUqh?ej=kH&|WIf1dc&m>LSd&me~d;(%Qm zafoPIDYDdR*r^yi-Ug@&WtG+|^r)kT`dHz;123qVu|iBO*<4m0%H zOrA4Iz9eryG8OVsjY$cl zn^#v?zpfM*<4|{|CT-m&#~`A66klwi<$&4|b1bXh0ajKYexjX?4&#ZxER=H8L4m`6 zzwlMAT*|m>)kEC^A_EbOL6LQQsFZ}3iAhPu#;l!EA-YZ+ZTT~B4h8_hLZg94us%B0 zBKQaG7<3@sSRk)}q@$*mqsJ0VJ|Q{S*^6;blybXRt?b=_umKq=Hdi6!31f3GvFVRK z!-AQ4E>rkLgn!EAJ78h_sO0+y_YLJ)UmJ6|ubjMhjCe>t`uh4(Y&DqYMx?SqG3}6) z0eci=<)O;BAZvJS8Y-a8z>S9xP0c$$-9!2ytHU(TFu$Syg#H%Q*TJ&zB`CYl6ctp` z!2$1w%><{8KH%<-v*`VXbbQ)kg<0&mmpjU_>5e0cT3Ueq5lF1etf3`%uJ~mjRQf&; zpP+3KdXxB~0tY<+H>hjCr8ZoK)vB-1%-l)IkcqR`Uf2ikRBi~Mjv?pMK*rPA4k8;TmA$9)qn2XiCm0B zK;Z|N$6D7BF@}ETKXb~z{z`2Y+l%S-o$6Ym1F9sdWfvQ>&DFW&Jw-|7Bt&ORX6&aF zQPYIH$?M~GdznAmG5LqTD%~z>K1&9A2s$p1)+21l7$Z+L1G+!$=}%Zfi07d?r+Svh zk?^hq#dA?J^+uaQQPOj0RtVrW>u=*pPksXoke}@6Fxdd|nUr0Y3dtRR)P{q0|L&X&^>va-lDG5)}{fKQwQ0+kQE=AlwLBkx3S+D(cCwu2~GTettFn$s59$AE6ZZ>*J z86cgFWUB!PU@typ_*2#m-55}5laaNMx#-Kk@|{3Xpl{hBcCLTzhwRv1w1_m|es66s zLm4U}#}O(?9Z$UgP%KwDTVGVf>wJ4u^hF5$%j-KaO-=vn-03>2gv>5zuTGv z8Zf!6k5-bh*D!#0{rcG)ia1?PS2DOj$f(~x12g8%LgU0m+*w{IG%W5n5a&In9~ji) z{mm0@dYi|>hMXAo`s`woZoXMOR)!P@Nm15Ct@~gAg9KDm~|g|Wu2auHn4e0+X#m673>)fbCkYJKpK1CVemvsokF3=;&ci*(aQh2PHL&k)rVI#fdAXwL*E^5PlO~pCTR>if%bFR z*tB**7d_-%;RK_HL@fAGMx?vMJOaO2t=u}-1$aXHMiq|G}r>Ou3PEIk@k%= z9dyr-CMUza+gedTdLa)L0hA!rd%PokKuUHUIkyCShRsOBN_~WR>_%^c0#n3IU#0c> zryum{kUabI3Gh+7{Ua=8!fGK-6>OgZD1KhB{E1rwjSPRF?I*{wiA1%V3B(4t-d_5S z{)p8jbl2%R#*${h;kE10dUb0s7L1-{oFHaw8=YWez^FZAk%sI z(S$=a+qst)k?Aq;Wwj9lj$r`^Qa=F_Y}_o#+o}WVHbeNa-_5hn(idIL^fjkE3of8v zAo+g#e!Sv!d^kjtKmDN~gOui_uFNkC-Dv<-rsen4z$!#k zv@PW~ZS^GS>n^ITaEKvzxj~?ff^vaIFB9SnI1$$LPqZ>M7g7RY^g;Av!piYyzpl&X z`t{Fypn_;x5}YqeE@lpj~rSUgZsP3taiCqkW_F(uP=*;^9 zMPGuqpZ)NTm!%WbOEKPU@^3)r{s=cjBNWkf`l@)MvP)<@?ri{B4oGk~(-Z-V25i$s zEkBU~U?`CnA3KjB)sTdqTo#eR$#Q2!)^u~ENfdeHk-mo6b=9rlp$EA@)41+8073No zX#3Jm($V_ZLTIos&iM;MHx#6Iw>`+n;0quX#zeU~evdg7M<{C1ZNii#)+E#NHkQH! z*Nn|l9c$fjV!|nBvoi5VC1B^U}sUSd+GDA}nlxj)7CKQi{F$fnV?H)^7xGw!LB~e;X zsd9U;q~i+Ov-%lh!lFmP>DEZq!c?7yi*uQJ^TmlqL@mKP|I^b*o9$(GpAK*`2xsZ2 zHeSn%c_;K?=8^RfIF81U+*M)|Aj%-f%HwOLM9Sh)#kXg3W?(F};?+foBp{Rm*$Mil z#%oQn{2TM?Aepo4I6fiQABK(f{0Bd@-9dNPnk8bbt8aoAJ>~x_Ba3FpzHXOzowFsX6nWnkG;J5n;2?H132MUC0_DzyW*+lHm~r;GY|vikYe^e-UsigyZ6 z^UIdHfkG&o1TrrKeYp4gJ-Bd#p(O?Cutp*vQN1a#IaGT>HXv-if(w9p&sBa}sG}~> zNuyvxehfvW3k=_%KXyG2aah!_0v=y@em>2b74Mqmc#7xI583Ye{l>flwb3WS6JG^s zJ1Y`~%L;;8ehpW?+S#xqFX`*=ak&yovL&CHvQZp}NOx1?yat7@>^o9Rx zvisK@*n5dk;|IHQ7;ah*ogId2Z*tuFnOXQlp!p=f!i6MzJ|g)W-L%c?m_1^UFjhb0 z)_6;r)mM$ZD?Hc#NwA&_t#rr`j_V>D^Rfl~O{9SkbH0GUd^u*NXK*SLeW9QRKrwQs zpake<@0?A$Akf_}Qi+yai0hK~pe+KP>M)?$!%gjuoO&Ljf@G1*4AZw{Y$9~I-M@27 zB?V&|?u`wdN+D;e!Nh}@nsL}(sOs}!@KyKy{S_&@u`B;6lZ1(`Q4_JcJL?%Ls0x}7 zNcO=S+5<6z59T_qdZWKwXC9oL7`68j!N{t>@yTRlssW}Maj&$`V9Ab~h~=`Zwnb)R za0fkP{nj!3G^);Gp>^zakUeE87q-$F#LY95#GhTmb<~IF;{M33*|8l0wjSTi@})14M*&7`TVWgRbz$rv2fUV$DHwAo~LO0{XNK*q$JQot@r6 zcTJXup-}~XJp`(WcX}u{3_14U^U<{cWGpH23JU)V6R1x?)28nIyAf#1#km6&1TdA= zdc4CDT^$`{J&f)37`+!LzbBLX5+hV$RKTgd<4Qqu^MU@dPWNr{$CcMTeR8E;O@FZ&|n_>@6H9>+x_X@!;F%2b1tKIBydFTvrHO5i|16h zP)-XN{+<%rCE~P0?>{t^^7tu-X#xQW`yK%$jS|6dTA?88ge)#h#0bjDN|7c>kcMsS z%osWG82pdq6LE{$3Qsp18&Vfy;BEzlZ2ycsMU5cP0Ttei>1ypCmeZpB3MlF161#C3H}~) zdBsp`Hh0Eu?r*eSSnc3f+j~>}?Nras!bT{Ogu0SA2S94Q)0f+Qf-pk)y@`{P@WL&W zAsaM{`F!;5|Heaf0=7L`yZPL#O5;w}^?xh6=l!+YvWfFZ4{KK<68m_cj3 zS~9Ru1%G4UGPpuQ1hf3!T#p_-GV$=xM|9bDilq1WclN4|_A*pXcq?=}dW-|Ef5yjZ zj6NAg>lLnF1M}^qhK4Xi#LUb~N=fvNz<7^tx(}k)M(7M+a#GOHtu{k%R{HmG?uzpFkhd>F@1l*Ah#JGIc|n|QC++HtV2<4Vr3 z)91FVPe0}zK2C0y#Wg1BzQ09~&V!QAEOvWlf8SWg=Qlyi>(^dYJNI6e*@<6Unq4un z+#W3CEU=F2FhdQANli;|M@%FgdTsue;L376qTjZp(yo!cNh)iDrHVy-PvhoAGKIGH z*$O(s*Q=twskZU;-?G2F_hzJPIMw6wm2X0on=18X5ibn5qEb)9mOo=-ENtOwT_O1$ z|H$-Fv=OeeBt4`{X4Xa56YGp<+jqW5uhbp!IO5wyH_P;#AS5I-8fM=a& zq_=DE9{ZPwcSy!mL{3_o&K22BPm-!TRN5*>r%N0{WriIa4u`%7UAA1_$ZH)tSuvX3 zb0)5$KT0*1dHr4VKBuaQx{}g(+6#rWTP^g|>8E`%u9otjj@IuYrl1bI%b*ut)7N(n z*q4m#YP;DSo-9D(? zXmoX+`^n17(TX2_^IGv0N&Q&`T$B#knVxxg8>OAZRNkYI@iDf5O9w93@!GE+{Ui=Sp>bhHth2)HX_l4;#}Gyoe^o#U;8;2CTW*+EoWz+w|=Z@e}7Tu?%uk<)?&(g z)4HyQ)=3v5ZR;Bv9OrmXDWp{`OuE0mcX(1w>XItmsqnj{o;x2ht z;^90QX(=c=8o}a_bmUSvsG|G}6N1GsBq+Ds3jxlqMYYsBswsM(i%l8fE${ z@(Y?H*d3W4d!KfijM*V57=4N{o<@eU8B+cPrpqW<*q1}Zth%wT^7 zMp+vh9#F4XgOS@YVrkmC#TI8lixXvbf%aXi>IGF{%}?b%-F*)mh6xAle{}f@YLT>< z%=7Jql<*H*us`g;vsX|^<8H7Zfa`uki6Ql=ihfs73VEW|fhy}>sn61@#9!=v9qO}E z<}NM?u7;xX*IY+nK^J&X<|A zLGh@xU z`1vDA^n=7iwMedM5+8>Bp7pisHlH7}xUAI@zA$95H#jyLsribmDr<#pxADhl2RC(q z!)6Bz4lmj>cq8g1^@Wzsae*Q!{#E^w)upQnbh?2bSWmadyt9`!ux~p``~o9f_nrI$ zn{iAi&Zl9gxpV}3zgcnNIDIQBC!^Ypr-Flxjg&o+m5C`@gocr^PQzV@ZEr0yj7E+izlX2oNvy zXwJxu;RO!0r=|{+ND?t=xQ~B(o};iFnT1D_bs@mX@6@RwZMH5DJc#(@Vr?Bkpd%S% zV!lhK^;Ri`3+t9XV_N2;r1YP%tv#|H-ojth}`+I^YX zd`d7f`yvRDX8gUbpzur9R@-^d`q#IgM~Y@`JKdX==E{b59Et3uP3gh~Y-NRfd;$Yw z&ExhN^o9BfgaVuQd18FNw=BQHZWijcAVPs5Mxj8K0`#B!w5{(uPno z)DqogZYa>jR%D%D*yK3$L>08Bl@2wX~va@Tu$mn0UvfQ~YEuZF<_s8D9 z+4-)rbC|yNhD(3A^xL7X2xb)<(4D&d980DOYbHonH&EKHUWrV@beuVy!M^51!rt1@ zUSwR{`4iaKWR#SqT^%O#PGVX~=0n;Kt)4{UkitwlTcwqg*D22N@$ncjuU`Nv1u51V8sV9>3TkSn6+>t~)K@U$MvRKe+<*|xsSz))=^MdGOX0Ofsq=NpM-phLKx zLCE&B$x`5}p3M7jyX7P#!}}Dq0@`96v>(h%W_x?1%2)EOukK2UGBC*e{%imNLN*?K z)~>MP2+_M2!x>Yz9Yba59M9(56)Rb?Q;F(zI51$g&Rg7K(kGuR7L|h1*vyLiA4O99 z%lECpEh;E@WpTAPJrVy^qqn3R(y$vgV%>Huwx}ZnxEi*MW|y$^K1d&>mqv}WE8yQ#IYixwcp5GC z>RbUM_75)?H@CW`CcLAa4zHVc@3LE!78f^=#ZYHma9iSyWab|j9yZ?JUL$?kU@;6J zK}1xvrlW%dbQ#V@P%bPifPN)zyohb>xb5J;0OE-QA|olhf3Xv-t*rsjq8M^^ahZTJ zEIgEi^C}9I56sNWc$_(Zv51IJfoG9(aS=VobuX~6kS}2uWJAw_Oa<@Hk&(4LuLD9* zuHkDzIanNxy3EqOqT-d);bvo#0Ttx9riov_0?NunU|M+bjwJ<|qfjA987RhFk?5IZ zFyq5%i1^lZbc1&_TQc%c>Kn`D{D>1|G5if!1uuN}hwNlNmTQ^>Zmf5^Fo>c=s@6lu zQd8O+RTyhRnw8LAkoyWZNq5ywqmo9kS64?4jBCZg#f{zH%UN=2EQ!_99bS-c-2Qpz zEl)=7LP|y1!zn&ZE9>{~We=|;5UO5(yYKeS@oq(^~>)pb>QLRwc(-`QE<$e8-Vg%IPWkO>%o+lU_}Rt5?m z%{g&dwbx{2v0yYUXgztuwnWy;DD>zyOUaxD(N<-~NIC&x8Ll5#W=BQN=6f~;l9EI{ zza;1RB1-x-%{dZ=i zz^Ew(`iZ^!`Nz&mMZI4%+((OAAkyWb`$lr=ej^S+rfjk#qkIn$Y9P2xJLw6&ACxF@`C zZUk(dYmU~`DtZ0b^cHev$zpCf+Dc^|715U-7TwQX@cC3L<3?>Wdf|GcY}U(ljZ}GQ zEos)lWw~d5FJ6!;9}F{yrlxP7dwldq{mlJ#GpBwEa5bN9>grx*;sZ?mET*T(*(VPPPZTj_Dd#3knLt51&MV%Z?2qO5+b#!Z9^9Ew^q|w(1HHh`D(AG69HY-!Ox871_M6 z@%&P11yM8aw7j&`m-LVuyPf->1FA@8rl?@@yQj@LD=2FkPgq#EXjdZYG;V({gmF|C zX%;?q{rMC2=Iw{|;<8JOC$N&-AEaf7og1w80p#;{p_ed{Rb5E8LBqVE8+P5~(W6rZ zF%BJk>IH6Im;z8%&;usK2#@Ne!ff)?Gom-B^`i#npAz2EPEX2$7QUVF~ZAbq7DuD!QA=L7iLnv2`g`;a74oiL^_ClhytX z`N4JD66tv%`|~#x6?#VJBKJG{9^jJnm8(D2r8cc$^?j+XruggGNwJMJI|qf2N%Z7J zMZssZnwa7;V#C4=yOfD#)~mGa2L1(QupkB-|L!k^;e;&k2L6mRVr z{<49{6?vLPTFB_Iki%wZzB8NmLWzk_mzD(RKsh-%KdcrhDJd!u8-@yp`P7n7B_>er z!uI}k3JX9^qdbYEU+{Th8iA6=Mnym}*Oom86$1A6+rzI@UAVx-c?1%^8M(QAiy{yU z)!Ko>)}y0zlp+sOFdv2ktV$YWQAj{6W1ndbr)WafMlUQO>fY|SE^oQ)ZrD&~9$_=9 zXmjJG&+1RBCi!Am9g9*gpJp*L_kNR=R9t~Wqt$fN(Os+PPVq^{{+x=%d$#&ob~ja) z0|OnH_sTOD_~Wqa;G8HYSr83(bqA9YI6f_(nNj-wP3~h=mD+(e&Pm?zl(T0z1i?-- zva@Hk{Wfi}v$v;UIrZbslGXe#&1IPU`|Y08+waV$0o5SDCL|Cb^W88yC9b%h&Nm=i zjCe9%+EzGG3{5}$tqQM#_EKDxr(RKMX%nd8`fb!bCt39P2$HSMba(W~gK*$)*2;7& z_UBy!#ibRIA@TF)&pU%N9T}n1uW+aV<=R#7hp8zkH9aZR2}`EYuhZ)FKx-2;r(L-{KTZ2n%C)+u7O2Kb)!ZSj7Gp5$G3Ng{1}bwew_IGWv1O%RFt+ z`4psHkSF%S_HtwTK;uo{4#5OADs0wE4Vg9(@Uv-s#FG863he$z9C?=a&K}t+zq-X|;0rMFA>1kS)&QabLTZ zuvb8XP2F1Z*8%Z{4*G@cz7x$Xg3T;vqu0kCbPYfxN(y=!OeO#skDubREPSNqDVdqq zbxH~fGzJwp&l!VYH5)hgH4uWUF<$Mr6}qBJOiT=a(gYXJ7Nq_FL^pef&r_;jqa-J1 zB)U-LxTu{?j&Bkv1L}POB^w(XS9Nr#o;`ck`9Yoj{Q-2!l6~EtB&;+xe*y@B*LG3? z39><`@LX1m5-9YI!?-3s4F~IMy|aCw?jyKa@adDn&6}hke;catPO)ZiQ1`Kyygc?B zF8voEt4AZCeYKFp2NapuCGa1&S`ZhS&?~4Iw|@UF;QSufx>$&;cHl0>+w@b|3KeGN zt9EYh7x%&r)eskf0{t#O6a3rmiMN_kvgBkRJz|MG5g3?J_i}Bcw_N>!_BlK88XS5P z8aSi_ZxTc3!o$TjjJ0sTkZDa% zO&tW%t2Hex*R9VN=)3|QCK}0b+3+P_Lwh9)7(@W{#sp4oF zhvPi?W#!gZ`MU;*YW*MHzPkk66T>KKt5|6;hTjwkoi1k9oYA^gJOkKLD_i@bnAlKH z%6~^(gS7^jpevwjYdBu*gYtenQjuu|TI^IPZ-4)jKnoEiHrtF>V-cMfB0?-K$nyDu zVCX53-?J9(CCoKjEoEn5=Wb;K`#8KD+EVBD|9#;!9lG-G+0wx zo7yl%C+&G}vA-lgzZwLcSEnP@zcJq)0M=x#Glfdbfeq|AVy~k5C;SK~{CrkHJX=9o z`4luJXFmmsyW!a;DeXZNte>PFTXx`e?ypBE zF#&0(F*Lil_;P#OnTS?|6qrzFP^Jx!i_3O&2TSG?8cG7YhtM>$?R1)2T3FsU=@b^= z@UT-bS2!*PKt_xhjN_BFjSCXa-d;z0V|SZOiLxSS(t=J}tMX>b+CH*yOtTi>HDx4= zk+#oUm4o2MM9pAl#Y0Blw$F`E$Xp9opTZhKstM-s{0D(#nOS1&s#bHL3qGj%EwZt} zO$&*K&`-}fV71fy!M_{n_I}TWM=n*~*!U9M0h3j#tgNinE){O-{W9yTV#zcpBs~KN zi3a)!v|@?N-pzryeZKo*-<8Txeh@?~cWg)tgyUxfGOu>S?Ck8TQ=!t6pdMG|vd*Hc ztn8rX3`V3Lw901KX#{{(0mVW9m%d6&CpzQ3!x9pLK_u@oo&2UCWN8c^KJ*tJb;gF+ zNNXoH1X%KhjBjPOQ~4IX?C(Wx-M$?j8~Xw<$2*{3NG?cp&X)y<(4+D}&JqU)hk=z9 z53Z8J?b{S61_lPylgf>AR#sLh0k3qc!NQZkufENGl&TEYD&J;8?nbP@b*`G!f&wzo z)foVRYnywZN~`&PN=HX$-0Nt|%fEv!6L>~61oGGwfWzt!it2>6aTMmafUnFo5s-5){8fRO5td+S^Z~RzXcYB{j8{$u^dM z&>oz}2}(*z#8Jbj@tN<+^iIZy|pq{#mzYk(I}#; zsPaI>>reu=?RiCon1Fx)*s7~5(eE#K{M9JmfT&YqZ>cC}mzrw99pw$@-22x8Ar}`H z8z(11iGfmU+H0%vnHhi9BzJPqeheBKz&%YHh{)M;*(!dkg@mfCES0Do)9DKmY^Ca2 zQDL&axR;rE-9fm0d+uH6Q142{lKeOWVJ>drfG>|F#xKEC`jb?uEH@&Mhpw02y*3QBl#k?hhniDl36F!9r!^1uX;9J_p|+MNLLBK-JgFM2ZW;E%tYOD(piW1d?RY%!k!bQxMB>^YxoI4}cdAQ)05XzX!)gi`or{ z$2CwiZPhBw$jp>AH@{3nLo+@;jtWvZ0qz3y<%+kZp1|?W%3^nz@47m0j*1FjU0oeC z65ll({!+U89;DM{qubhlM$&+SO;}9Kpiy=&X61^Vj3p%;egn1GSToU+Zr9p+V#k;L zUoAj*bd%?940VT>7W^HLuq4w{o$melHDJ;tZ%vl{rp#YhX zL5z%rjf;Ps2yS^btW2g|wG59lpT1+gYpgJR-~YZqK_06)FS z4#7f=R5;!idjz|JfC_L81jD$*G=6a2ISsz({4O54WPYoTkoyZT*k()+r1SH4_ZVe@ zj=bptDLP;YR3g^L4N$RGQ5TkDQiz?mAGw~*7q%0weKBfun6HnC3c`Q84mr%shl}`_97?e4fFdR9+IGFiFQRGo5m~au#7kb^J5ueBy zjxS^XFaUr;Ex>y)Sn@6|<=T1YDJh?mT~fqfgnfYHSp$)f#{K&w*REZwZ)}`|F)$Pc zmT-W}oYzvj3IH(ixco6Gw+5C|5Rnf(2Vj(%1pi-CTl>_?h$f&6_=A?`jhH*|hy{|u zx82Jg>K7A5kVrB++aH7(6 z0|HQA*^o+5FdvQl=S@-Vdweb}t<+TifyM(>v1ZzZ~b=9ZRBW3}gM zCOA-Pe$Bx_z;$ye=oo=Eh0j{~;;W>*x>_YdSbqWE!e=K#56>7##p{ z1q$N{jZz?FTL*|M>e1>GE310wNCn7qe;mHQ#oqAX-~{OIbd+!0R zKFVzj+)ERn<>^QJ!YocsPW#JGyy`&v_;uC;6H`-TApU7Mela(nN3I>PE`9~50pNk2Z+*h;hm(ty}f*%K_w|EDdHFa zO?+}806!ed(ErRfdCv^j4;#JnGVN%2LX5kG^2^cOge_>%^$$TkBz&Hn zRNS5MgnNb{oLbih4;ZCjUR%zn@5S=ZpN%116D{V*&O{#c^Q-jXHa6t*l~h!2FPP4) zBNPz0Ej4qf0pb&SGRj$M4O;Zhi#eqz6sRUQ!fsD*!W{D1)jxkupg8qPh^h`Y%6?YCMX??Vs~dFf7w7K?J4o7E%yCi(oev=LV}`xHpy$$h=L{!GJSw` zhNcDIy?ZCRM~B%7YJ#=D^|EKZj7yoMM5%)Y2Ni+V5Z}G`x%}Zn{OxH9$Q?Gas9J?_ zsj!O91N_Zb%}P#2fkl%A#6OIu{BI)?-gqsYD+`WId3w|mOqRjb{-iFjS(8(h4n?MM z?Jw}{gh50A6hMda!qerU-Un4sas5%ZNKxql+=<10p&%?2k{JY8IRz%Y76>0+#|2HW z0P;-a$l_B8Kc)pt6G$ii6c}agIaWF<5)_o?)=D3)k+{S1fGMs9rQ{zV8x2l-^3P0b zb91wdt7}E0$Qitr*{FNT5Uw zeCW+NWN)UY(>^VO_i~<1aMFDI*3RipyI7Ru8!J6sXxeVPfB1*8e2d4h8dX+SRt;Go z@a!TWvfUVco`xn2ur(my?cLo;NF8uAhR_kB@(T)T;HvUi4+*$$Pdpz0vGhjcVpEU@ zt(6~1OH0%4u=%iTi7@bx`q~Mkme9%byzzkHw#spos6`EFU9!pfmMo%WknY6-O}ZO& zxahx*{Dd;F_B4@HsA}_SHqE0CQz6&hN%MyL1UBO%a1%LqkgrCez9)!NLUi2VgN>(I zvIWV(8#jm$So^jf&bl|`J2djBP7x9oKpfz{IcVxU8N|v1))>;+agWW5zkttuusU9oUsxEC zJ$KP97@Yd$4HA6(YS0rm1Skn;Z5rg=8UdXJ!vrpDRJvNi&;v%_kMZ%eAMcfu1IOqL zy2QMm`_7OqLPFNT!iQ86&OD>eQ^XKLS$KTkRN7?_!5~=(@Z~i%U(4P%Zi!&VmArsi z++qWu^&ki-Dttvs5dM2IG7?5&`>1(9#GcQN{ci)vsN-{ruD!HgigzuRWimNk4^ zQ}<=w`a*Wz1GoTe93bH*?(}*S{`%D0`<^ur)1y>WR1h!#IxtXAg=`D36(|8#nj0~E zCsE^oydfC5S8jK8WoxXb#}6oRqXVe>5P>n(qfOm?a1>WT@mhw79H|CT-h)$X1JdA2 z0Hh)0MwZCR+Sb*elixeUHQe)QCYBxKMh zw%4X0>L&{sey>9L<>+v4Zmdcg_|m8>3zUqlEic5X2tfze3_ps)-Q6Ab{RfM*vHkz; zz@fGzM+&l>EO4%fNHGSEj9M9@ne^@Lxt~wk3XGeta0^4QUvW7*$KSL+muhk$%aQ(? zp5AoS+o}DL&x)^J{fF$t;$kP=|0aFWF^=nMtxAUMvh|t&L)mx7bG`oWzwJV0?}Rjv zgs3DdEmHR0qfknNkd=sxBD-NGGb@y`lGQ{JQD#G;P$Kht-PJkg^Z5Pud;f9Hsbjp~ zulv5A&+BHt^vg9U5hIiHcS21OJ!83u}45Ca7fH7W9XYfDRt9v#J}5?$4V zQ3pr10<|v`khJ1r-E0fON0{I92NTdQ<@bwegQO%`SD=VFq;-1bRr9k`d*Ga+q};qo zOAH7W7B{*E$y>qd(oo($ICF5Lsx_#dp^k!8l$!2teraiGlF^bpKk^cCC#w!elm?|0 zDJv)~-ddMfe|VuS{Bk(NBWD#wKT?8*0NXlk}s^oHw#ozadoS-@QttRkXYWL(?@xPJQYpK*0<8RkKtoc?(2cp-RMy9<9)k5{Jv zALDP1Sn|LDH~`gW9^XI9-Mlzu8C^DKVLsLmRVnn}MGaAqn9{jp?bKoLq3Y^@+1A;| zOfCYfiJ=6?JqCtnb+gAO^FY|M0dy;}06Pn`S$L#l<9lQfx1;OUtrOVls*-=@5fRCL z4C#<{0|?rwrL~Mw^W+I5$#IIFG8a~?+E}s(C;+u`2?e|nH^iEVe2c}oE3Q9BOvt9# zdiLiA@XFRsPD@c90wY=1g%Fs{gF{C}!S*mNag1u2$}Ol0!11CGJ_aDU!th(J9LS;b zRX($fU!b{d_4={CxVV@EOH`bQX9&`%xDAtMhu3C8Dx~_ruS&LFP2ne!OQESIDETN5 z1)F@5!P!E{38=CFI9FtR=L6QlG8ldRl%I9)9uK;?_+BLKkhuG-(ZHN$v<2E}!&7TKLS_jVKru7q>QV1<$76eAo3GH{6sA zQ6$SBd%Hd_I55x}TtFcxP`FB>aKo=r`28hFMEJH?aSO*IFfVqNpUNye*7O}1d8}pU zihclk)5+vbHq->PTRd>@2Z0{b%?*wjB5G| z?Dp>iyOB8ce%H`PSw>)l^_6o>6SB)UT}+L(5ust#Vdw(}{giZeIhGV@_GQjNgo~=V z2~F=Gt$Q?P@4JtN(lK%M!TRS?bOpoLIrA+ZTp*%5s07uWJI}xMRaRF1@ug$M;iiJ-kRH+J?BlcN6p^jkJgm0w+c%I z(pKhwPA`rx0j7UmC2P6Diaf?`6&I!lVL%n{ewH9j0}1#-Wu*`Qo;~F49-N*TZkLkY z-dz*L!po40((a#ERJO@g1Qar}u4(niA_$sH@fZ<^kXH*_FQwGf?ueg`W&YY#*Jd8jwzQ5A~2965S4 zFlg8J(F@~)jXO7R-E}j@1HCQt`q9n;Un`4y)_r?J!ZmwynD2`!|MzZLvmb#M{quSm zrY_QY&uRk~`d(8b(u74sWWqu3-I#qJ7yDUJ=z5bt-MV#O+IpV0vPz$4;1`1p^Pku7 zen<|#*RPH0^tYasAis7S8z*G%Om*J?s-FC@+IPOKtf;t{EpBvlH1;P-)u*>}ozY8V zpKm)q_hb03-XnOIY0TuNL zm=4+ZbQCYl&l33n9-dpvGxV5NtY}2SVx#~Z?L_!R84=s{c&#`vyz|?Fe67QWaAc6V z22g$j{cItW^G)kr+L4y?#u4Yb?qGM@Z!}KPOXgHnt@!%KLO?c}!N~O;eM-*s_kV?} zxr|G4SpA}d@uj!TMu-2OEvU173bYb9P6{#SwzO#f299_GQ8?LpnFoY->{tmS#@bN* zH{evH;fMw!O9N(l0~k=PmAaX7PA^4m-gI^G?Kt~WKe4F=NYti0KmUMJ^^F@h-rmoV zzwriXvj6MXOJL!S_EfxS>qm|XH&#~m*rqW2RtilA<@=j%y6-oQRgD*C`a_l5%lvmh zKG#1!%yFS{U6acxemR=?)!f1^gQ9YkS?NWDu>G@~3dQ@FLy>l*VIF~|vXmrXQuY$} z`i2e6adEerUA_TLP*M&iD~TulF50q6|~C35$6UfzBOvp4GW2K%JJYM=bV$;n9{ zQaI>(<_ts2^{p?7QDqOdKrv|rX#b|~TXk-=u=80YSB0rJtgJSHd;gwDad$0JqQT-IW0z$ZU#Il|%POWxgl=>EY!ivorw3DfZsvaD3LIGtwfbVqt2m z%iC~c2G3OFb#wFH|2czuJ}7y`HO#;B-d(06YVUk2**E&n`~5+775D^-3dq3&$x0Cf zk<{1MXXWGsq^GY2T9!sKAre)VKZSDOz=4GAXEwsjUW6OMN8a3F(9+Vn3fGQNeH2(EZF75eg?u7G71+q*Kb%-{=JEw#pp^^8t z!Z){ZW_iPFx4tb=+q&bD@=vKXho%Uv{r>Oyk2-L@Qf{4ir^16rpsm{L^FrRX!t(h0 zGwN(FJN8Z?Fer$Mf;uYl`t@)aInpr?ANla!nQwyl%fho$$Li|p`hvJEYXH|i(8rsZ znAqU*>drEkYx|;?avYw>EF-QuRGKEo%#f%dP3V{1g?trfutuh&BzfWH7dZ?R)1RK6 zrci)0i+tz33=hGP6^R4qH90$6OGiSt0`amueflbU#s7rbsQjF;ApfojO{>M6t0?+#QY}iW`zdl~uPc0(iobmUFwHtM)_Nsr}sI<-G zlhVvu>VLO}&L@5EUq%tLOfm~^c1K12Peyoo5Ons{t5=2CFvkGphv}50xOgq{_c*SK z9*i}zdGA&KbW!2|N`mFqWLJz9*^73L?-H)P@AlQK@(!jhA>+9)=c#Wh{ z!H#%oj1~$|UMnbIf>!lG?Zyu7=K+vt0827`R#m-?QCh(}_)2U-tOJtC{)48m!ga6o zz9^yHsgsrU90AEZy#GD~H;y&&vaX|J2Yy_uFm|%i8kc;3Sm0kkOfrXaCj1UMrlyQ2 zVkrI@8DISd-l4?-&dUby31R~4AlQ(;k^Fpl#|jGR8Tm2V&5cqXH4P;GtiomlNBlnL zSyMB!)o4(mDO)xKjyfhcMJPjz3e8N0#)h#?`22T_>_K&@f)2-nyAS1+xH3mEWs!82 zq?J{}$O(IwsHhCQ9kwu}Z}k`x`uO}>Ff1N}h_OiE%HcCgT~{ClR8&+z?xe$8j){%! z2hAy7ASWWSv(=%dEYIchXZlNlO^U>N`D;p~?iD+NZk{d?^Z&d|%TG?Pf-^!Xz3;s& z8$}xpH19?8RozZhiS106uR|(MTb_biX$ce4BZ2|1UUeRJ#m3HBhJ{)RijO|UOS>F* z^5tmj>*MWGChiRLKe^Ma6bAWKH&cnoX=q+@g<0}#g(v&s+&ay@d{5v}mG+)SG(t^C zP#j1B%JX)mw$Cp!!V2?-uO4x#7TdaYYYKc=>mP3$4ym`TnE%;*^07h}3f~&k1+L#e zYJs((19_|O91W!df5#`TAN{4PR08ZYE8;F#b(AcRN2NAyY|uNnqy00x9lUmyD7p)L z0oR)3qWwOFFiT2G9W2OU27HkH7j1=vl)S$FY-~m6wzHc&Q=>`x)Y<+CSq%j(iEI=W zF0KnBBS)|AILpN?Wx4spY5NU?9e`BzhIzTDh{&s``MW7OIgBdbRVIMxS8v+HQoS%E zL8NbZXkzDKJlF(_2Q$wmmCm@>*fj#0D-k5{5C|y+GLDd-_+8`4i*pQk_6EPakeyw| z=ih(&GzbQ9=doiO2~|O2xSN}sKibg1lkyow+Kx`c5fH5LqFYd;*OIPiQR)U}mpqcETfV;T$NNCz?-xAX# zaxF_+M<=zU7vq4ND%Mo>^t)6=Jqvh^|}QFbA=vhUKkP<--(23Y{oW`hmf zeM^>!9UODi%8(5!$5yRe8Ih7AM0tMx+nxx%AV^|_G<1~U3L|tn7_D@CpxU3HY{*XK zvwDT<9)AOk{_b0M*eeNO@9*z7En*F4~Dh>;{yT5;Y{%Y=mfvdZF13;gS znOT@r^&H!$7dP~}-Cy3^hvpP8(uuMRU>9P#l~EW8H$<)8BLW8(uAS|?cLG%+k8p=P zJ{x{Rg0f##q1Fbjogz_Ss?DeOZN6s8H=#_L}=QW-A>VzH+kefpAA|X3K<%j04ZeH!X5*iD? z@66|Q@TVI**fM2B=zhfUz`=dn6clV<=a~>aB2LQc5*~w-TLrNffTM*(L-?$(z15dr zzSQO5;u=7?UpLx)H+BZmi%3M-_xHGXsBd!8PT3E8`+$k%u5PpjM(b zAzEbJh+et(R@0j|yoBG}?1&qbuLdF_39VjDv*(h)Lli-9+x1?Ex{^wa*A(K3B*rG< z1#qH4x`~DuRa<=8x(NHxoF5&Xz&1o0A1pd~@?;@^2b^nJF0zP}0MjPN41JvpmV8b* z0o9w(h{&Cwn_0W?QI?yP9fjYy0UO#CBry07e%QR}DV2wtF2jy@g_!^uboG?FI7CYU z2&a`;xpR49%oE7w{)jC-+S~6`rPuZ~VjYmRC^)9Z_gqsqZBqPSSCf%C(SJTRCtt>d zGRfX-8~6oy;2~pZ@qS(bQ51nCUNf}y#||BN9aV!5v~Rv;UKN)Bj=?!fh;qRRunbv>tBXeut}7O7d+@3m zJ|O>L$52gH^4Wy3c;*k>myykN0#iat8~LSeX?wiBND^N~yOGr{Q`4)ic@Vhl(Cehu zjn+I;13?%{e|ZA2@wctTLX+b9_3buj;=n6edfbzEu4V*}TzeamFYkklqazzbI%@$Uje)9Wwd>M$P7F zoMbKYcSz2l+SV;!sX$9f*m^<)7FtHEIL?sW`=+R^Ck6srs!^7ryvutl4W=D z1`DvPOP=oV`q9$)8s=MLLpeRvuTwz|FiI;f&V4_*j~NtCWBI91+D1mfV8@}!HBC;9 zk5jcMc2|UjF`=A9dvYj@WJ~uk>sne`dQ3cCH@=Vmg)u*PJCwytUsm}}r)rO?{5)Ln z`LOV{tdy-1%0kIQDbCfgv7Cf0R~)ZnH?>NC`9y?t7J+NZEVHn;7d59dJR0GYu2( zbTcPR2CgbF_()s^(eqD5{u`Q>5?8zWXaYVxSYs**>@$#`HWOM5t{V`7jPNCrjtS^X z(xd`k>zCr$_Zs>yE0PjQacj_OwS9Y>l{pyvLBxDoy?v{t0?Rz~UE@kGxz`0&E5L1J zC;_Ud+t>)B=QnpU3+KN1Et7 zR3~S3E%u!KCT3vcG1f;_H8U(Bx^CTSW#v`Kd^|GtOGH+$W@Tf8tBe`Zlf)xn@dxgs zx(Z(-g?APrp$lJ3g=L@Ig6d_P70)9k9#PyVY-okk?OKGp%Ld=4(XFSe zTj;mwOVkc{x`@KGc7K9D2vVk{;($j1=&?gmJP(LgzHs+=FIQYf`(6T7fpTOH;9%~* zzLgaMaD2Bf8mlwPfP`QZziuuA)mr*?i?&PV8i5q88@rBqZbOV=`SPtmPZ_M?NJxmv zZ+FOls$7Ty_2?}nz|cD-|KH{EErgiAkcQ_JezXZ zC?(5`-j9rIh|e2`=^@vJpNng4{Du=S#AV1AcSARYba@WVRC!j_RrR9uDoRB+Q8fSl zffIGuYei?iS=u1Ve^nbeFO3@(?I6I_o(bmPa?Bc$@1%=;E>D)GedJn>z1bT2j$*Ag z(A`dcA*gO~u6fZKNv7%f^Iedw5DbWV2BKYf?b_Y>6ZI$)f}oE-T?rMapX^It21F6! z&L+ovs@HE7>|47rh(>qUvJ_AMg)*1!Kv6W@cq@K?CObWHrx_IaIwObL?rt;Vq^&2? z-hN9_DDJ2@8pL9;H+ngoQy-LQZgMoAfOqC~zC{@Hi-9>zY{HGLZf*PKu-O!FRh(m>5lg>8X_#U#Y-`;oA?Bu#>o8k(x9S3Abxl2@3)ZYF3 z>);qEQMe4AHRHJ41@J_KtmrAr$o$eSh3e1J`HJVioMA`^g;;<}&5$3l-D+oGE6FMH z0Uv;7V#0gCL1+iX<|NJ2-IoO;RwzxY6i@})E6ltY&_wsv9=?F9d!V3ia!lc#@k z?nl>^jyT z?Po>q%mVtK-G2L}VJkP95r{N^X^{kRm>6rzKyZ|c3aBj1a9t#5D=c#F>CrB^--b0{ zJF+cOZ`>c|?)9COwDkJC2KaDEx4_Dk1lxHdh$YB4FtYQ7G&zF6?wjm=B6I)c8SO1P zX9;ImQlWTb)^nReSEa72+C$cXN%-3||MLLWVSnp;1J68Ye2`E%Nj7>LB)+D&7)LN=gFI zN-L~#c*;fE@f^BQb!Xgs3C|Fh)lT{HoykW8DX{+qe!w0O8;ws|N4e+yO9oiKMByty z-+S@f>#J71sY}JT37-(#vV~h}?$7+8;dz?rVBVhw+N|0tbg^0--0C{ar=C8$9V62r zDpyL)z4^9wwe8nXv+<`Ho6nS(pL{G=zf9`{6=!{r)-H!=Esp7V>E|vRWoc*vLVSA) zjHJGu{JQPa@|~84Uwl`z@(KF!<20oPK3R2TiTYlqU5F$^WCs}8Rs8_Y6tb&es}Dr_ z%*j(wRsSKQwDB;Djk+53fZX)iupzBGCXWtZ9H75n9{XVCfA&`8FCG-&`dXY^T#QUX z{u7b`_x%=?pwo(*5;Y{FHDI^6vYwuvhlfWBnmu5>+>@=k9@w)6=1fAaZNTBebcWh( z7)pS1zzJttAkyeFgzv;7U_N#+&4z>wJx@<)wUAEtJ;AVCpaxF9jZZFogZ&?Sl+_J4 zmYiTlBecy~Y%ov{SG&qk+$>qXe7C**&9$$%9qOd2_F6`vB83N<78s)SM12lbXxLrO z_ADKmg@px;Mgz%wL+_2${@87~&$lHY`FquvO1(rQ3aJQO<_*emd0WnoM73-0-Vl}$k<5aE6{y`>=RyxR88Mqi>UnQ8n3R>1kzp$fW4II} zN*3I*Li^3-fwR+i7S1%L8W7X@`_DLIv`U&k`!3AY;*mWR{&F0@JI-{MigZfW%_(mr zz46EElC#X~3XHd@x8J#G`ysrzqQO6pG01#;>Q>J13UoyhJL|3v2M33X3(7XOwrTDa zka9`DnxC0`1gF}rb`4On#79b|^B@uE<|yn`*B2ahad}!r?+OHc~SS{Swn#ea9=Q3DJOe>m89c^A_+Y*gKP*Apksp@j`JrS&Wa!SP!=d-8=5=N=6`s3<)Ls}a9hqU!p$Lo`m2Lin6k8pfS&%UfQv)8!I%Gh|l z>G}fW4>e6qOB56o_zY;p4vbeOp^LvbA}L7_Mp`lb)q4*d;PSoj@#9Cw?lMU{{horaYxN#*XUW_m*uuZvUmRJzWuwr~F_>b~|e`?rS>u`UJP^6+bZCM?4 z?W%sgY}%6s6_Co#`%kdODC!larV7JYi7})r&^u2?x9rx?2z~Me^Q@$8>y~VGZri)n z9gD1cXSrsE?rB&e5FY$-+J5}_F*ek?nozJ1fsD3W8qC5GH#|+-dH`@+{eLsjL zRc~)aMD_~+mLXiEK1H z0X2L3w3e#sxyYsOAZj3@!pNtluivKGY$eU2s;S9z>(;Hb96gG5wXaVq+fEqk)jyGJ z36@j%lsCqlkxbd|4X_%N5&=$$fDS?)QcSoO2hOdk=N^uav#`<`uL%L841~Qi=A6|= zFoHPhEgc=fckY}j=kCcx#wAG^##>#NYiVgBir&*K*Z~dTH~NwP0H2sXb>x|(Z(dg) zx@6)+sNT`^;Wpij+Ubm@OBNo%=)V8=g*$$myQ4U z!I2Ze;nz$vph6Fdbm;Tve((de()F{)+Ok-k+pg z;maXe_a-*f(fec5id0?SCmPa2d#%7iV(MG-M7~9<_vA2R(m&{km9AvC{LWJ8?`bCl1=}2CqIn$KG%*j(l>ehu{->WC~I**sS(oD%&;$Dp-1m- z7*{+DFE)5&Yc74p!zbmglT8(>emKA)06y#x*7y$Zi`B>=C!9?GxVeuVN(n{4~(u5_GcAUsF&5Q2dd@28qcW zU@h_&ck@T9%mXVk=sH4q2MGLDFoLCt*k2LuF}?Ir@T0ai4$v}=t!Z>KQ{Pso7#2Cg z#YY@Mp`oEE85wkl+xN=LFD99VN@c|}Xgk)TDlJ*W84OE$2wM;*x_|lbz5*{@T8@An zQ`{SG^!%z^5qd8-AMMoM>JE(0!_Pm6M;#I#t`18#nLfj4eOo7;9s+m}^8%zjjqg%| zW2Wi5mt~A|c4v7J!fFK^GgK6?th+JbjSS8MSPbnlF*PNH2GS?H9zuMAkEf2LlO){=INr1rI7l$kXOBmj z1mssx)fZ%x4ff!c3dg>r9=RWHsT{QSmrd`A*2ALtOzy{;_86d8A< zt0eoSCu#AdrR@Wig363eU4KdOIO#Y;v~}gpCE8w7pv^u!P<|KK)<5nAutpA1&;df{Gp~Wm*6`gz(w9 zb7y}-g?ON1Zhll0>!D`}=SZnpM|Vj#n**_w42IC(==!R?os*g#PQ+kP@dGd#rpbdw zR##D>l$4b8p=F$X-GM&rQ1M1tV6O&jK266qNJucj!MTpCYh9iC&5Vp}<^b&eNGRVN#D$3hgl0_$tmlC|pavsy#!&P6{`$ae6LsfriG+%v z)z;a-xg&oWxI7+S-hKoI@x>m@pi{B2NgZQ;g;>#A>jHQQ*BsL<>Nox2)ryJ&P!n6) z+k^0sNHkQCDhL-U5CUN=WZ(P^VyiCYmSPK)f25}sJ6ccyw~Q?`UcW&aPIGt~N3jp< zl@N(cg)&c!wk-ZS`uc(RdtqGR+I9qNy7>Jw*fSX_7b+?g!PzfCsYg%2Sz`fB!c}_l z@?~zBAqjfDrcv1C&(kksn~7A;UAuOz2z*BO@88d|cu@A_a2;v70Q;apwGMsJ2|Lcp z6S$+e`pfWe+TWt!;?(@yuTzBi_gkFbl5buf0IAf&>&cT?1qec5sXt$Z1Dd2>oJ@KO zX|p4adwubs{QQk`b90buZ@DXO!=8c3N(8N?TVI!-I(&MhlhuF32mg}JP!d%3-Eb6q zK&W^rRdG!$&?{5(9XNODz*GjJ!W~3>g7&p(%N8TvtWy{Y=l%Wp7V-1GoafhQC{ys#>~vD{*Vzt;XLXT1fdp~IY_&cbMl9SHxzlMXa7Af>h*6*KId}Or8ITjk)q-4vCha00V~%L z2aHxI*&OGEn4J-1vF7o!XX+RrfNiN~%d!(YdDGdSM}6W+Y zMGGfU9F-R5P5~7zK`DfDg&8mOU3ZUPCe1T|fsKR@!YB(gvxdk#GzGt@camNq!7}x_D+-n=xuJUn-Yf}Nl9URUY>E0n(=i@V61r`>`qimIbGm+GWGVQ%dnxk(?} z+g2ni`)GnKDeB+aVQAFe`k2LlC(=a1cCV_mH!Zmh#9`W=0G_2#a)sqRw=Y~E6GFiU z<(hk(0j<st5K=ECE zzP?K6yJ$=5Mb9P9DxHeq2Pb@+h6|+7i;$#pM+Y$!V|wDLlr#!$_{N2llmtnZB7#~8 zi#L{OYSf7zf)@gL!}#^&e*_`&pBOXpd*U5I@GMi%^p%!a`% zb`B0vS_%v)cF1sBPreUn=-{^M+*n|$^A=!#9Ggzf%4$^vpIiXu!w&s=kIJe6eOsj5 zA)m(N&Y?6FHkGmjKalgJb+82m7~W$I98ybkpM&5rHx~d<3&x5`7+laMqfoGm0)Xlj zP7hJ=@X@R*PDlBKQPxn~A@xL|F*CaK|(Ruku|&aYQ)-Y5!*1G4L>I z*85N3SP%l3#G=@TJ5UHpLp;Q;B)A0$<1BP`S$9z8p#f3^iRJSdTcj;<{Z=a*#VB- zT1aPLMbsTGKpjaVW)`-071@mNEk$?^GbxbqEhZr7)Ekjh|dqoI)YQuu6QzsK(t zB`liF@#9hg{Bq7Q>t|&t35kiAPqb{4oE#lRV7ogr^l}YV4pI3rk4_&4LY#RT_NES*BTq=RPTv5*al-ayF5V#}qU9}8~W*c&J{2H^b z#KZ(*)kR8lON;wx+1k;3H?& zHstL`W>U+zh<<4iQBhhH+Z**(p(uluF*b5?0ki^Zl4s~CP-4Ns3~1kSCGg{WTwENn zxd0vzx?Pv%aIZX?^oX@*|Ni|kFCP0H4rTDm(#7FH_eCi6zMN}NtovY6VqaL9N+2ic zC_MA^ZFXHEP-WeVHV&PIg%4FdwykN5*O%!`mgV~SmE0|roSNh>u&lpVy^vD_TV(4O zbXYT?;k5>zBZ%=0SUD98qsh3`eeZ-FtSGADu_B26sL?nKE!POLXlS5P{W%}rZ`HwW2cfWf%a*%! z(l7wh;J4cJK6}OtR({MV`Z1c!SWsp^0%-_Dgt{w{SADk?Ri@Sb%Q%ucEo%*u z5K3UXEX&Cp@49#TGaF8=e)}t-v@LBaTkf8;+WzfXBIEU?I+NT{kz=|Y-3V_dyUP;J z&YY%u4jg2;4#g7ud`jl#=IV{Fem7a>Yphx%h@ly6Qu!MxZ>FRX1gEj*txOD*;UWOh zv>MpFqrl2WiB`v_`vOPpPxT@qPy)3 zDGc!7^0F9dDY55RL*JZjb8r^Kq_ix?!0SatMUI#g_`0^}PgjVE1Lu-*Vs=H0NZ((G z17AZ1@Vk!~3n+SM-{S@=PuMU}Hw2KNtxRK-$$$M|84_%lJ1AYyS&>g>*Zq!1B3}H= zrzQQ$KZgX;)*U`%*mpD}r+WI?v-stQrf+r*7>bB^$zr^cEz(Mw&;`(gd8Va|#Gj7P zt5JcwT?uKA1H5tk!R0GAGcyUJnf5LGw3{0%2gl8yXR||pyx9KD8VvN`j(*Z34>Mn` zsrNz5h&aDHf=~J!*YA{SaPe)MD44;E(XtOj4!ycNefXEF`m>RM_jjs|Cfl;Ej9aDR zQ3JN{;Q*pKSOZO?lGjX9>TRRDB>82+{02JGzBvZa~Br zAQO;T=orDKzI*q;{%tZcmH@~``R1w^_DHB<(vMDP1{Aeu`G8M}VFcazMBp|#r?yeg zhBXK>2B4*`P%!8tR2}FGq(cJ(($?)~-`D(E%ABtO`rZvG+0Uh=Ik&Q>ZE1X5r(R+- z{j4w#G*U~?>z^4kBG8&9WuOdhcIndZ#AN9jRQ;sKxU{3hrJD;_Jn+VimBX)ZOPH!B zBi`c}5;xDhtudgo;8jo-4itX3Zl0N)GfLMb;Y}|b*hjf9Zr6RL3gm8#a zM?*Cb|L$FY8z_(!nC2x-OUMlS-8wp~@L-d=q@Tv}SMdFb4RT=GHRj*JVvk~wj&gda zMU{hMZEfw3!jo~8*J3Y35m3rOD)hgSL#M<(jf^m>aBad=uuHHP%k&=E2MbRjIt=Ha zj7(#6VGEdop)X%{0wBO@bAgPYr>0gv#}~?R`G}L#Lr}G!KYvb>hr6BI@&T$?gQ2;< zzLK2t=koIlWo0F%Ce7`&^gWf_-9LWgVQuBViBfRrZTX!-D@>dQp(UW~YwmCsr=3*q zpiK0-GttdJx)5Qt$0`kgdnJ7iV=!N2&o8KdC8R$p63h=06Ck!tq*dMQhcxxM=br8p zWrLOVA(T8A4|fsQ9#YC2I#@7Fm_nki)2Wv+STafqG%VvH+n|?I2mbOav8uZ5>_Q@G z6KDVO+iK}wARh$~R|4`mh2rAkLY4^Pv%WKbdZZ6hY%4StsVQ%aW+EV)adu{g*pzo+ zll8&aL@P}YBMN-j_)1#jB_uSnt6|_@+6^8BzFJ|0Ghf3IMv3z|5d0pc56}_JLj3xs zbnxcR-#}X$EQ$2sMMb%zYWC}Pqff1wlpFh9z;+tbA1J!3C zb?Z34s_;BzGYi7=L!#0joUc>*vnWinyx^V3r1$`*xCmRTUxkE_Q^QmJ)A-CCVdu_K zk&HV6?hANT-GzmQ0@v96Iw5)kNNjx=YmMm081_}O70_g!F9i%W4UdFyA5N$z3C_;1~_FCK8 zQey`I@dL+I&rO6C4)p@epdMFobuG-|3lzt%zW{k2^^7aTeBnpW(VCcw6N-{Z!Xg^> zE#g@Q(Fs`}Y;o@E@WX=HO~HPnP%!Ta)VjpGPw*25BLTDqERaA7Ma*NF6lSnu zY?3g=dgA@#me;QvP=aVy9K?cec5I=bEtYhZ4Ga!aHHj-V-B|&^$qV~hJycOdkleOC zNc*H|9Rh|8DEHb*qWj3m=w|z1@C1nW+CE!LPZ&!C$F-EJj@~2o;`(~NkxmCCb8itG^S!0==Fsh7tpA4}9iYBwb z@Y#`ZczAeH4%|?H$P$$GQWemg`0Dp3D^=D>gLEV08TdiXt*@*77Ii-vT4GnJf^v_- z)Jy;+qUPK?^=Uaa9Hz!oFv1ENdn&N{P+NBNP}0cgC@TyENMOW52mq56v(5QQOG}|f z5%N=$Y2*5h($cITDr-qffFq;$y^Fp+2e$VG&^R=al7)*HTT_Tx zW2D-Hknb@*K!=KmQ1@WTQUEH_j!L4GZ~XQB6BPx3h49Dlt+eJCNI)1F0Q!SBs0KAe zd)yBLgD5HrkqUtHLd01pDu|+_B?siE;@O`lLqPlh4%Z^K!9@z^GC7m3&*4_{$47iv zS4YG(;)Nl8JCutwP-)cw!XbWPh({Q9RXVfs$A;o`a2NK zi5s_e0b3SBZ_nu#JOEFIs6IA5ZYQi7qkxHNczk79FWs>so3HkQwOw%C@VDPaDF+Ix^V@>-|Y;1KO;ZcJ+GdM`*Z-la)PP}$)H7a33 z-on8hl$Mspcq8)`i1HkJ^S>BwcGK^I-~osS%;~VkPe-XDoGtK%8Ui)}hP0SM$-59{ zR%A;FrJ#awD^Xo#CpEh_uz=ckp;WKU#Mh*e}z0)jRT3V>G#)~Su#pG~;}1YC6Ft|n|7P)r8Sr_h=PC$jP2_+KishlA$b+b2FQ-n;4J&Wtju z7$ypf2&+1{d!DiEZoh{qBYWg2GI zwlcqkxosFx9u9#&9o9F}u&C{0j(v$AxEoU;r7^M&j!eTLb2%MYX^fQC{)yd{H%efL z$z2$JG=tnnLLWV4-jfCjxZmRSK5|(6(ge! z-(KHVn+ShAa8d+P5@ZiA|M~TuS-%~bu?4eeVMz(C5EC^q`?}(8frwcQ0i}k3zl`EB z^Fs_kD5Ffx!xJ>G0DuPPPq4NT2tx7Ymx04zG}74m6NrY)OXZYrzCsXCr|;afG|Y*h zCYE8ATRKL+ZNjHxOLIVFj_Dx}(LHJT`0by)H1xb4f{}y6?{3T;{0~%~J$v^}M+R|E#VDPO%SPsyj)QK$U}USx(^e2) zh+`i9XA>&f=zRwd?g8T)G>I7=WGg+*o)C;&+P5qblLV*U*z#$%-KR=p%!@G zXf>JUV!$*TR;}bmTY9=u zr~$Sj3mRnb=xHe6(Tgz6CjRQF@r66Z#epiMf0t6Mu?*y9AnF1#coj%f1pK1IDpdQA z2rJzPyk<1q38xkr%iMzru5GzSEBrSU4uCK1#5;cJfiIPi3&HJdOW;y~=YrfIK@K7G zpG-9x2@p4Av_6@qgkPjT^JWI!aJ7L;ah3?@8U^7VvqvJZc^|%gD}V@AgE!$i)FO-m zB5VIC5R81)nK-u>h=2a=+lsmcECVm%8F}f`Gh>cZTE`WX=g44%R=g&M|E8Rw=P!F+ zgLTpt&j?+@td?@OrQ)RhD^QvpVelXo&f|CE@gaI>s4`<30hn!m!h*g9>dTm6@JGKp z<2hz>?WRF47o_pIm-9~`WG(H+$lTv^tO-1(jKOr;hZ3t1{Necf$Ly)M!ASAR9}PkH zRuhk9Y$RnXH1wFQhhWGe#7Rc|Byo^bqv8Mb=K$Q2(&~ZZkT%SkL*Nur8Q^?qAL>i3E8YS)w!X80q zu9b#i2;|QI1VRnFGw=?QJO_6OF2V>pUiFI}!iV@L6U1H8z6)PNM$aP1L|~rJy4`;= zU#lN+Fsm1=AyPq>h9l3WTV@B5?wi{st4Y`EUBEV|o z-2-lD0lQI5lsAB7WgmTjS$?QwKMZSf)(g@8nqMzSV?Wgfp+I-G>%kc*E-6d%fiDUW z)rsK-yjpEhObiF0z)mRh{&^yG*qP(#4qV~_N(LlVAjWA614R>p1DwXD<2{P0X*)60 zlm1NJ28=5}2!sKbw38R18>rNU5Sd4}fIbn6R4vjavLmp=CeRS2?V=nsHgMgY18>9$ zpDCPUG&s2ZPzXyf9y1ljd4yb?iiNOL|^6dXSWb6^jyBAFvb8el`y z`_Nhm0hY=JVA)OaiJ=!mdY0_oy}Pz7KmV)dq#N$K0D^b~mgmDS(^6*}jHe}|D6kpM z%YMijIW_y!rPUW;R(x}$s-PleF8=x#JbB00d5H}^7}&Q0I}VfPy)TX;Z?7>8Pexb+ z`Wpm=yGdStH^ja_LkbW+tq&c#fDweL^GFM1nyF4Q%tC5HJ8>mM!>PDj*bq?s1kK}3 z#CG-|2zH_yTVJ8Tc%SOQgDVIlO(#uqY`m1!Zs@ioNn+!+c1}1i2fNBf&5QU%Sip=T zGD!@%ZGb5AgFtaOptw>^4Llor;h#Nez(S9voz^4S9!qU5s_X8OMb=5n(Y

%X@CT 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 WI(lGHugh5qD(f|MH4@d#&-@>Li&I$u literal 0 HcmV?d00001 diff --git a/db/migrations/20241030231410_alloc_table.sql b/db/migrations/20241030231410_alloc_table.sql index 57cf3a6..233a25b 100644 --- a/db/migrations/20241030231410_alloc_table.sql +++ b/db/migrations/20241030231410_alloc_table.sql @@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS allocations ( -- This alter statement adds a new column to the allocations table if it exists ALTER TABLE allocation_requests -ADD COLUMN request_type TEXT NOT NULL DEFAULT 'SYNTHETIC'; +ADD COLUMN request_type TEXT NOT NULL DEFAULT 1; ALTER TABLE allocation_requests ADD COLUMN metadata TEXT; ALTER TABLE allocations diff --git a/docs/validator.md b/docs/validator.md index d8b2378..cd847bb 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -40,6 +40,18 @@ You have the option of running two kinds of validators: - [Synthetic](#synthetic-validator) - [Organic](#organic-validator) +Before we get to the differences between them, and how to set each of them up, we must first ensure we have a connection to the Ethereum network. + +#### Connecting to Ethereum +All validators are required to have a connection to an Ethereum RPC to handle requests. It is required to interact with relevant smart contracts in order to perform certain operations i.e. calculate miner allocation yields. + +##### Preparing Environment +The next step involves interacting with an API. We've provided an [.env.example](../.env.example) file which should be copied as a `.env` file in the root of this repository before proceeding. + +##### Connecting to a Web3 Provider +We recommend using a third party service to connect to an RPC to perform on-chain calls such as [Infura](https://docs.infura.io/dashboard/create-api) and [Alchemy](https://docs.alchemy.com/docs/alchemy-quickstart-guide#1key-create-an-alchemy-api-key) (click on hyperlinks links for documentation) by obtaining there API key and adding their URL to the `.env` file under the `WEB3_PROVIDER_URL` alias. + + ## Synthetic Validator This is the most simple of the two. Synthetic validators generate dummy (fake) pools to send to miners to challenge them. To run a synthetic validator, run: #### Starting the validator - without PM2 @@ -77,15 +89,6 @@ Where `ID_OR_PROCESS_NAME` is the `name` OR `id` of the process as noted per the ## Organic Validator This is the less simple but more exciting of the two! Now you get to sell your bandwidth to whoever you want, with a very simple to use CLI! -#### Connecting to Ethereum -Organic validators are required to have a connection to an Ethereum RPC to handle organic requests. It is required to interact with relevant smart contracts in order to perform certain operations i.e. calculate miner allocation yields. - -##### Preparing Environment -The next step involves interacting with an API. We've provided an [.env.example](../.env.example) file which should be copied as a `.env` file in the root of this repository before proceeding. - -#### Connecting to a Web3 Provider -We recommend using a third party service to connect to an RPC to perform on-chain calls such as [Infura](https://docs.infura.io/dashboard/create-api) and [Alchemy](https://docs.alchemy.com/docs/alchemy-quickstart-guide#1key-create-an-alchemy-api-key) (click on hyperlinks links for documentation) by obtaining there API key and adding their URL to the `.env` file under the `WEB3_PROVIDER_URL` alias. - #### Spinning Up Organic Validator The steps are similar to synthetic only validators: diff --git a/neurons/validator.py b/neurons/validator.py index 322d497..7cfc3cc 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -36,7 +36,7 @@ # import base validator class which takes care of most of the boilerplate from sturdy.base.validator import BaseValidatorNeuron -from sturdy.constants import SCORING_PERIOD +from sturdy.constants import ORGANIC_SCORING_PERIOD # Bittensor Validator Template: from sturdy.pools import PoolFactory @@ -247,7 +247,6 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None metadata[contract_addr] = pool._price_per_share with sql.get_db_connection() as conn: - # TODO: make challenge period variable and based on user input sql.log_allocations( conn, to_log.request_uuid, @@ -256,7 +255,7 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None to_log.allocations, axon_times, REQUEST_TYPES.ORGANIC, - SCORING_PERIOD, + ORGANIC_SCORING_PERIOD, ) return ret diff --git a/sturdy/base/validator.py b/sturdy/base/validator.py index 0152d8b..3e52064 100644 --- a/sturdy/base/validator.py +++ b/sturdy/base/validator.py @@ -52,6 +52,9 @@ def __init__(self, config=None) -> None: super().__init__(config=config) load_dotenv() + # set last query block to be 0 + self.last_query_block = 0 + # init wandb self.wandb_run_log_count = 0 if not self.config.wandb.off: @@ -155,14 +158,13 @@ def run(self) -> None: self.sync() bt.logging.info(f"Validator starting at block: {self.block}") - last_query_block = self.block # This loop maintains the validator's operations until intentionally stopped. try: while True: # Run multiple forwards concurrently - runs every 2 blocks current_block = self.subtensor.block - if current_block - last_query_block > QUERY_RATE: + if current_block - self.last_query_block > QUERY_RATE: bt.logging.info(f"step({self.step}) block({self.block})") if self.config.organic: @@ -171,7 +173,7 @@ def run(self) -> None: else: self.loop.run_until_complete(self.concurrent_forward()) - last_query_block = current_block + self.last_query_block = current_block # Sync metagraph and potentially set weights. self.sync() @@ -413,6 +415,7 @@ def save_state(self) -> None: "step": self.step, "scores": self.scores, "hotkeys": self.hotkeys, + "last_query_block": self.last_query_block }, self.config.neuron.full_path + "/state.pt", ) @@ -426,3 +429,4 @@ def load_state(self) -> None: self.step = state["step"] self.scores = state["scores"] self.hotkeys = state["hotkeys"] + self.last_query_block = state["last_query_block"] diff --git a/sturdy/constants.py b/sturdy/constants.py index 68c622f..9a55e21 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -28,15 +28,18 @@ STOCHASTICITY_STEP = 0.0001 POOL_RESERVE_SIZE = int(100e18) # 100 -QUERY_RATE = 2 # how often synthetic validator queries miners (blocks) +QUERY_RATE = 75 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) +ORGANIC_SCORING_PERIOD = 28800 # scoring period in seconds +MIN_SCORING_PERIOD = 7200 # scoring period in seconds +MAX_SCORING_PERIOD = 43200 # scoring period in seconds +SCORING_PERIOD_STEP = 3600 # scoring period in seconds + TOTAL_ALLOC_THRESHOLD = 0.98 SIMILARITY_THRESHOLD = 0.0075 # similarity threshold for plagiarism checking -# TODO: make scoring period variable and random? -SCORING_PERIOD = 300 # The following constants are for different pool models # Aave diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index fc9fde1..c1a4d01 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -21,11 +21,12 @@ from typing import Any import bittensor as bt +import numpy as np from web3.constants import ADDRESS_ZERO -from sturdy.constants import QUERY_TIMEOUT, SCORING_PERIOD +from sturdy.constants import MAX_SCORING_PERIOD, MIN_SCORING_PERIOD, QUERY_TIMEOUT, SCORING_PERIOD_STEP from sturdy.pool_registry.pool_registry import POOL_REGISTRY -from sturdy.pools import assets_pools_for_challenge_data, generate_challenge_data +from sturdy.pools import assets_pools_for_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations @@ -42,8 +43,6 @@ async def forward(self) -> Any: """ # initialize pools and assets - - # challenge_data = generate_challenge_data(web3_provider=self.w3) # TODO: only sturdy pools for now selected_entry = POOL_REGISTRY["Sturdy Crvusd Aggregator"] challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) @@ -64,6 +63,8 @@ async def forward(self) -> Any: pool.sync(self.w3) metadata[contract_addr] = pool._price_per_share + scoring_period = get_scoring_period() + with get_db_connection() as conn: log_allocations( conn, @@ -73,10 +74,22 @@ async def forward(self) -> Any: allocations, axon_times, REQUEST_TYPES.SYNTHETIC, - SCORING_PERIOD, + scoring_period, ) +def get_scoring_period(rng_gen: np.random.RandomState = None) -> int: + if rng_gen is None: + rng_gen = np.random.RandomState() + + return rng_gen.choice( + np.arange( + MIN_SCORING_PERIOD, + MAX_SCORING_PERIOD + SCORING_PERIOD_STEP, + SCORING_PERIOD_STEP, + ), + ) + async def query_miner( self, synapse: bt.Synapse, diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index ec4b9ac..45dd424 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -283,31 +283,6 @@ def calculate_apy( return wei_div(total_yield, initial_balance) -def calculate_aggregate_apy( - allocations: AllocationsDict, - assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], - timesteps: int, - pool_history: list[dict[str, Any]], -) -> int: - """ - Calculates aggregate yields given intial assets and pools, pool history, and number of timesteps - """ - - # calculate aggregate yield - initial_balance = cast(int, assets_and_pools["total_assets"]) - pct_yield = 0 - for pools in pool_history: - curr_yield = 0 - for uid, allocs in allocations.items(): - pool_data = pools[uid] - pool_yield = wei_mul(allocs, pool_data.supply_rate) - curr_yield += pool_yield - pct_yield += curr_yield - - pct_yield = wei_div(pct_yield, initial_balance) - return int(pct_yield // timesteps) # for simplicity each timestep is a day in the simulator - - def filter_allocations( self, query: int, # noqa: ARG001 From 2046b84869273188b2b1b97872d513772dcb9e5a Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 6 Nov 2024 10:13:27 +0000 Subject: [PATCH 12/51] fix: incorrect active allocation filtering fix --- db/migrations/20241030231410_alloc_table.sql | 1 - sturdy/constants.py | 10 ++++++---- sturdy/validator/sql.py | 15 +++++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/db/migrations/20241030231410_alloc_table.sql b/db/migrations/20241030231410_alloc_table.sql index 233a25b..bdea5e5 100644 --- a/db/migrations/20241030231410_alloc_table.sql +++ b/db/migrations/20241030231410_alloc_table.sql @@ -8,7 +8,6 @@ CREATE TABLE IF NOT EXISTS allocation_requests ( CREATE TABLE active_allocs ( request_uid TEXT PRIMARY KEY, - active BOOLEAN DEFAULT FALSE, scoring_period_end TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) diff --git a/sturdy/constants.py b/sturdy/constants.py index feb8449..8ea8868 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -31,10 +31,12 @@ QUERY_RATE = 75 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) -ORGANIC_SCORING_PERIOD = 28800 # scoring period in seconds -MIN_SCORING_PERIOD = 7200 # scoring period in seconds -MAX_SCORING_PERIOD = 43200 # scoring period in seconds -SCORING_PERIOD_STEP = 3600 # scoring period in seconds +ORGANIC_SCORING_PERIOD = 28800 # organic scoring period in seconds +MIN_SCORING_PERIOD = 7200 # min. synthetic scoring period in seconds +MAX_SCORING_PERIOD = 43200 # max. synthetic scoring period in seconds +SCORING_PERIOD_STEP = 3600 + +SCORING_WINDOW = 300 # scoring window TOTAL_ALLOC_THRESHOLD = 0.98 SIMILARITY_THRESHOLD = 0.01 # similarity threshold for plagiarism checking diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index de7bba2..da7e977 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -7,6 +7,7 @@ from fastapi.encoders import jsonable_encoder +from sturdy.constants import SCORING_WINDOW from sturdy.protocol import AllocInfo, ChainBasedPoolModel BALANCE = "balance" @@ -165,7 +166,7 @@ def log_allocations( scoring_period_end = datetime.fromtimestamp(challenge_end) # noqa: DTZ006 datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 conn.execute( - f"INSERT INTO {ALLOCATION_REQUESTS_TABLE} VALUES (?, json(?), ?, ?, ?)", + f"INSERT INTO {ALLOCATION_REQUESTS_TABLE} VALUES (?, json(?), ?, ?, json(?))", ( request_uid, json.dumps(jsonable_encoder(assets_and_pools)), @@ -177,10 +178,9 @@ def log_allocations( ) conn.execute( - f"INSERT INTO {ACTIVE_ALLOCS} VALUES (?, json(?), ?, ?)", + f"INSERT INTO {ACTIVE_ALLOCS} VALUES (?, ?, ?)", ( request_uid, - True, scoring_period_end, datetime_now, ), @@ -199,6 +199,7 @@ def log_allocations( # TODO: rename function and database table? def get_active_allocs( conn: sqlite3.Connection, + scoring_window: float = SCORING_WINDOW ) -> list: # TODO: change the logic of handling "active allocations" # for now we simply get ones which are still in their "challenge" @@ -206,13 +207,15 @@ def get_active_allocs( # TODO: the existance "active" column may be redundant query = f""" SELECT * FROM {ACTIVE_ALLOCS} - WHERE scoring_period_end >= ? - AND active == True + WHERE scoring_period_end > ? + AND scoring_period_end >= ? """ ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + window_ts = ts_now - scoring_window datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 + window_datetime = datetime.fromtimestamp(window_ts) # noqa: DTZ006 - cur = conn.execute(query, [datetime_now]) + cur = conn.execute(query, [datetime_now, window_datetime]) rows = cur.fetchall() return [dict(row) for row in rows] From 7b19738ff6ea22e538ad3ff8df634413df1ae029 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 6 Nov 2024 23:23:34 +0000 Subject: [PATCH 13/51] fix: fixed incorrect sql query for active allocs + temp param changes --- neurons/validator.py | 1 + sturdy/constants.py | 8 ++++---- sturdy/validator/forward.py | 1 + sturdy/validator/sql.py | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/neurons/validator.py b/neurons/validator.py index 7cfc3cc..834669d 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -227,6 +227,7 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None synapse.assets_and_pools["pools"] = new_pools + bt.logging.info("Querying miners...") axon_times, result = await query_and_score_miners( core_validator, assets_and_pools=synapse.assets_and_pools, diff --git a/sturdy/constants.py b/sturdy/constants.py index 8ea8868..64b43a9 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -31,10 +31,10 @@ QUERY_RATE = 75 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) -ORGANIC_SCORING_PERIOD = 28800 # organic scoring period in seconds -MIN_SCORING_PERIOD = 7200 # min. synthetic scoring period in seconds -MAX_SCORING_PERIOD = 43200 # max. synthetic scoring period in seconds -SCORING_PERIOD_STEP = 3600 +ORGANIC_SCORING_PERIOD = 7200 # organic scoring period in seconds +MIN_SCORING_PERIOD = 5400 # min. synthetic scoring period in seconds +MAX_SCORING_PERIOD = 10800 # max. synthetic scoring period in seconds +SCORING_PERIOD_STEP = 1800 SCORING_WINDOW = 300 # scoring window diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index c1a4d01..881d315 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -48,6 +48,7 @@ async def forward(self) -> Any: challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) request_uuid = str(uuid.uuid4()).replace("-", "") + bt.logging.info("Querying miners...") axon_times, allocations = await query_and_score_miners( self, assets_and_pools=challenge_data["assets_and_pools"], diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index da7e977..2277961 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -207,15 +207,15 @@ def get_active_allocs( # TODO: the existance "active" column may be redundant query = f""" SELECT * FROM {ACTIVE_ALLOCS} - WHERE scoring_period_end > ? - AND scoring_period_end >= ? + WHERE scoring_period_end >= ? + AND scoring_period_end < ? """ ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 window_ts = ts_now - scoring_window datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 window_datetime = datetime.fromtimestamp(window_ts) # noqa: DTZ006 - cur = conn.execute(query, [datetime_now, window_datetime]) + cur = conn.execute(query, [window_datetime, datetime_now]) rows = cur.fetchall() return [dict(row) for row in rows] From 1a2df624fc8ec419c9378e74b92a1ed497463a17 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Thu, 7 Nov 2024 00:21:59 +0000 Subject: [PATCH 14/51] chore: increase challenge frequency --- sturdy/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sturdy/constants.py b/sturdy/constants.py index 64b43a9..ac2d562 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -28,7 +28,7 @@ STOCHASTICITY_STEP = 0.0001 POOL_RESERVE_SIZE = int(100e18) # 100 -QUERY_RATE = 75 # how often synthetic validator queries miners (blocks) +QUERY_RATE = 25 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) ORGANIC_SCORING_PERIOD = 7200 # organic scoring period in seconds From 2b8e69ee22b3283a1368e33a0166d5263492b22c Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Thu, 7 Nov 2024 12:47:34 +0000 Subject: [PATCH 15/51] fix: apy calculation fix + increase query frequency --- sturdy/constants.py | 2 +- sturdy/validator/reward.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sturdy/constants.py b/sturdy/constants.py index ac2d562..2284ba6 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -28,7 +28,7 @@ STOCHASTICITY_STEP = 0.0001 POOL_RESERVE_SIZE = int(100e18) # 100 -QUERY_RATE = 25 # how often synthetic validator queries miners (blocks) +QUERY_RATE = 20 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) ORGANIC_SCORING_PERIOD = 7200 # organic scoring period in seconds diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 45dd424..94d604a 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -275,7 +275,7 @@ def calculate_apy( case POOL_TYPES.STURDY_SILO: last_share_price = extra_metadata[contract_addr] curr_share_price = pool._price_per_share - pct_delta = float(last_share_price - curr_share_price) / float(last_share_price) + pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) total_yield += int(allocation * pct_delta) case _: total_yield += 0 From d114c4585fa0aeff0b528cd1eab9b0b6803a68f1 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 10 Nov 2024 00:33:26 +0000 Subject: [PATCH 16/51] fix: incorrect price_per_share retrieval --- hardhat.config.js | 3 +- sturdy/pools.py | 2 +- sturdy/validator/reward.py | 4 +- tests/unit/validator/test_reward_helpers.py | 84 +++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/hardhat.config.js b/hardhat.config.js index 3e79ee4..00df9e9 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -18,8 +18,9 @@ module.exports = { // blockNumber: 20874859 // blockNumber: 20892138 // blockNumber: 20976304 + // blockNumber: 21080765 // latest - blockNumber: 21080765 + blockNumber: 21150770 }, accounts, } diff --git a/sturdy/pools.py b/sturdy/pools.py index f8b6d72..9ba3c85 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -525,7 +525,7 @@ def sync(self, web3_provider: Web3) -> None: self._user_asset_balance = retry_with_backoff(self._asset.functions.balanceOf(self.user_address).call) # get current price per share - self._price_per_share = retry_with_backoff(self._silo_strategy_contract.functions.pricePerShare().call) + self._price_per_share = retry_with_backoff(self._pair_contract.functions.pricePerShare().call) # last 256 unique calls to this will be cached for the next 60 seconds @ttl_cache(maxsize=256, ttl=60) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 94d604a..82734c3 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -257,7 +257,7 @@ def _get_rewards( return adjust_rewards_for_plagiarism(self, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times) -def calculate_apy( +def generated_yield_pct( allocations: AllocationsDict, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], extra_metadata: dict ) -> int: """ @@ -391,7 +391,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: allocations = json.loads(miner["allocation"])["allocations"] extra_metadata = json.loads(request_info["metadata"]) miner_uid = miner["miner_uid"] - miner_apy = calculate_apy(allocations, assets_and_pools, extra_metadata) + miner_apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) miner_axon_time = miner["axon_time"] miner_uids.append(miner_uid) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 94b82b0..8348c3c 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -9,12 +9,16 @@ from web3.constants import ADDRESS_ZERO from neurons.validator import Validator +from sturdy.algo import naive_algorithm +from sturdy.pool_registry.pool_registry import POOL_REGISTRY from sturdy.pools import * +from sturdy.protocol import REQUEST_TYPES, AllocateAssets from sturdy.validator.reward import ( adjust_rewards_for_plagiarism, calculate_penalties, calculate_rewards_with_adjusted_penalties, format_allocations, + generated_yield_pct, get_distance, get_similarity_matrix, normalize_squared, @@ -720,5 +724,85 @@ def test_adjust_rewards_for_one_plagiarism(self) -> None: torch.testing.assert_close(result, expected_rewards, rtol=0, atol=1e-5) +class TestCalculateApy(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # runs tests on local mainnet fork at block: 20233401 + cls.w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + assert cls.w3.is_connected() + + cls.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + cls.snapshot_id = cls.w3.provider.make_request("evm_snapshot", []) # type: ignore[] + + def tearDown(self) -> None: + # Optional: Revert to the original snapshot after each test + self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] + + def test_calculate_apy_sturdy(self) -> None: + self.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21075005, + }, + }, + ], + ) + + selected_entry = POOL_REGISTRY["Sturdy Crvusd Aggregator"] + selected = assets_pools_for_challenge_data(selected_entry, self.w3) + + assets_and_pools = selected["assets_and_pools"] + user_address = selected["user_address"] + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=assets_and_pools, + user_address=user_address, + ) + + allocations = naive_algorithm(self, synapse) + + extra_metadata = {} + for contract_address, pool in assets_and_pools["pools"].items(): + pool.sync(self.w3) + extra_metadata[contract_address] = pool._price_per_share + + # move forwards in time, back to "present" + # TODO: why doesnt the following work? + # self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] + + self.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + for pool in assets_and_pools["pools"].values(): + pool.sync(self.w3) + + apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) + print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") + self.assertGreater(apy, 0) + + if __name__ == "__main__": unittest.main() From c0d5863e4adaeb90643f735b7bcd1ce79e302bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=96=87=E8=89=BA?= Date: Mon, 11 Nov 2024 18:28:48 +0800 Subject: [PATCH 17/51] shuffle active uids --- sturdy/validator/forward.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 881d315..4cb6f5a 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -18,6 +18,7 @@ import asyncio import uuid +import random from typing import Any import bittensor as bt @@ -126,6 +127,8 @@ async def query_and_score_miners( # TODO: write custom availability function later down the road active_uids = [str(uid) for uid in range(self.metagraph.n.item()) if self.metagraph.axons[uid].is_serving] + random.shuffle(active_uids) + bt.logging.debug(f"active_uids: {active_uids}") synapse = AllocateAssets( From dacca5ea814a8350137f92088dfce91d4f3fdb5a Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Mon, 11 Nov 2024 16:06:40 +0000 Subject: [PATCH 18/51] feat: draft of adding back support for aave pool support for more aave pool variants are being added - since some of preexisting pools have a had their interest rate strategy contracts changed --- README.md | 1 - docs/validator.md | 7 +- neurons/validator.py | 1 - sturdy/abi/IReserveInterestRateStrategy.json | 355 +++++++++++++++++- .../RateTargetBaseInterestRateStrategy.json | 309 +++++++++++++++ sturdy/algo.py | 12 +- sturdy/pool_registry/pool_registry.py | 38 +- sturdy/pools.py | 337 ++++++++++++++--- sturdy/validator/forward.py | 15 +- sturdy/validator/reward.py | 5 + .../validator/test_integration_validator.py | 2 +- tests/unit/validator/test_pool_generator.py | 9 +- tests/unit/validator/test_pool_models.py | 212 +++++++++++ tests/unit/validator/test_reward_helpers.py | 69 ++++ 14 files changed, 1248 insertions(+), 124 deletions(-) create mode 100644 sturdy/abi/RateTargetBaseInterestRateStrategy.json diff --git a/README.md b/README.md index 58042b1..1f27018 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ There are three core files. for pool_dict in selected_pools.values(): pool = PoolFactory.create_pool( pool_type=POOL_TYPES._member_map_[pool_dict["pool_type"]], - pool_model_disc=pool_dict["pool_model_disc"], user_address=user_address, contract_address=pool_dict["contract_address"], ) diff --git a/docs/validator.md b/docs/validator.md index cd847bb..b1abd35 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -208,22 +208,18 @@ curl -X POST \ "pools": { "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227" }, "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b" }, "0x26fe402A57D52c8a323bb6e09f06489C8216aC88": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x26fe402A57D52c8a323bb6e09f06489C8216aC88" }, "0x8dDE9A50a91cc0a5DaBdc5d3931c1AF60408c84D": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x8dDE9A50a91cc0a5DaBdc5d3931c1AF60408c84D" } } @@ -239,8 +235,7 @@ Some annotations are provided below to further help understand the request forma "total_assets": 548568963376234830607950, # total assets available to a miner to allocate "pools": { # pools available to output allocations for "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227": { # address used to get relevant info about the the pool - "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", # if this is a synthetic or chain (organic) pool + "pool_type": "STURDY_SILO", # if this is a synthetic or chain (organic) pool "contract_address": "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227" # address used to get relevant info about the the pool }, ``` diff --git a/neurons/validator.py b/neurons/validator.py index 834669d..fee5845 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -184,7 +184,6 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None ... "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227": { "pool_type": "STURDY_SILO", - "pool_model_disc: "CHAIN", "contract_address": "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227" }, ... diff --git a/sturdy/abi/IReserveInterestRateStrategy.json b/sturdy/abi/IReserveInterestRateStrategy.json index bc50875..ffcf459 100644 --- a/sturdy/abi/IReserveInterestRateStrategy.json +++ b/sturdy/abi/IReserveInterestRateStrategy.json @@ -1,4 +1,104 @@ [ + { + "inputs": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "optimalUsageRatio", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "baseVariableBorrowRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "variableRateSlope1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "variableRateSlope2", + "type": "uint256" + } + ], + "name": "RateDataUpdate", + "type": "event" + }, + { + "inputs": [], + "name": "ADDRESSES_PROVIDER", + "outputs": [ + { + "internalType": "contract IPoolAddressesProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_BORROW_RATE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_OPTIMAL_POINT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MIN_OPTIMAL_POINT", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -20,17 +120,7 @@ }, { "internalType": "uint256", - "name": "totalStableDebt", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "totalVariableDebt", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "averageStableBorrowRate", + "name": "totalDebt", "type": "uint256" }, { @@ -44,9 +134,14 @@ "type": "address" }, { - "internalType": "address", - "name": "aToken", - "type": "address" + "internalType": "bool", + "name": "usingVirtualBalance", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "virtualUnderlyingBalance", + "type": "uint256" } ], "internalType": "struct DataTypes.CalculateInterestRatesParams", @@ -65,7 +160,122 @@ "internalType": "uint256", "name": "", "type": "uint256" - }, + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getBaseVariableBorrowRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getInterestRateData", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "optimalUsageRatio", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseVariableBorrowRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "variableRateSlope1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "variableRateSlope2", + "type": "uint256" + } + ], + "internalType": "struct IDefaultInterestRateStrategyV2.InterestRateDataRay", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getInterestRateDataBps", + "outputs": [ + { + "components": [ + { + "internalType": "uint16", + "name": "optimalUsageRatio", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "baseVariableBorrowRate", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "variableRateSlope1", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "variableRateSlope2", + "type": "uint32" + } + ], + "internalType": "struct IDefaultInterestRateStrategyV2.InterestRateData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getMaxVariableBorrowRate", + "outputs": [ { "internalType": "uint256", "name": "", @@ -74,5 +284,120 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getOptimalUsageRatio", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getVariableRateSlope1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + } + ], + "name": "getVariableRateSlope2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "internalType": "bytes", + "name": "rateData", + "type": "bytes" + } + ], + "name": "setInterestRateParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "optimalUsageRatio", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "baseVariableBorrowRate", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "variableRateSlope1", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "variableRateSlope2", + "type": "uint32" + } + ], + "internalType": "struct IDefaultInterestRateStrategyV2.InterestRateData", + "name": "rateData", + "type": "tuple" + } + ], + "name": "setInterestRateParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/sturdy/abi/RateTargetBaseInterestRateStrategy.json b/sturdy/abi/RateTargetBaseInterestRateStrategy.json new file mode 100644 index 0000000..afa962c --- /dev/null +++ b/sturdy/abi/RateTargetBaseInterestRateStrategy.json @@ -0,0 +1,309 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolAddressesProvider", + "name": "provider", + "type": "address" + }, + { + "internalType": "address", + "name": "rateSource", + "type": "address" + }, + { + "internalType": "uint256", + "name": "optimalUsageRatio", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseVariableBorrowRateSpread", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "variableRateSlope1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "variableRateSlope2", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "ADDRESSES_PROVIDER", + "outputs": [ + { + "internalType": "contract IPoolAddressesProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_EXCESS_STABLE_TO_TOTAL_DEBT_RATIO", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_EXCESS_USAGE_RATIO", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OPTIMAL_STABLE_TO_TOTAL_DEBT_RATIO", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "OPTIMAL_USAGE_RATIO", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RATE_SOURCE", + "outputs": [ + { + "internalType": "contract IRateSource", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "unbacked", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidityAdded", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidityTaken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalStableDebt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalVariableDebt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "averageStableBorrowRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveFactor", + "type": "uint256" + }, + { + "internalType": "address", + "name": "reserve", + "type": "address" + }, + { + "internalType": "address", + "name": "aToken", + "type": "address" + } + ], + "internalType": "struct DataTypes.CalculateInterestRatesParams", + "name": "params", + "type": "tuple" + } + ], + "name": "calculateInterestRates", + "outputs": [ + { + "internalType": "uint256", + "name": "liquidityRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "stableBorrowRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "variableBorrowRate", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBaseStableBorrowRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBaseVariableBorrowRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBaseVariableBorrowRateSpread", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaxVariableBorrowRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getStableRateExcessOffset", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getStableRateSlope1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getStableRateSlope2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getVariableRateSlope1", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVariableRateSlope2", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/sturdy/algo.py b/sturdy/algo.py index 17cfab9..5728209 100644 --- a/sturdy/algo.py +++ b/sturdy/algo.py @@ -50,20 +50,14 @@ def naive_algorithm(self: BaseMinerNeuron, synapse: AllocateAssets) -> dict: # rates are determined by making on chain calls to smart contracts for pool in pools.values(): match pool.pool_type: - case POOL_TYPES.AAVE: - apy = pool.supply_rate(synapse.user_address, balance // len(pools)) # type: ignore[] - supply_rates[pool.contract_address] = apy - supply_rate_sum += apy - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.COMPOUND_V3, POOL_TYPES.MORPHO, POOL_TYPES.YEARN_V3): - apy = pool.supply_rate(balance // len(pools)) # type: ignore[] - supply_rates[pool.contract_address] = apy - supply_rate_sum += apy case POOL_TYPES.DAI_SAVINGS: apy = pool.supply_rate() supply_rates[pool.contract_address] = apy supply_rate_sum += apy case _: - pass + apy = pool.supply_rate(balance // len(pools)) + supply_rates[pool.contract_address] = apy + supply_rate_sum += apy return { pool_uid: minimums[pool_uid] + math.floor((supply_rates[pool_uid] / supply_rate_sum) * balance) for pool_uid in pools diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py index 587f4d9..2e6824b 100644 --- a/sturdy/pool_registry/pool_registry.py +++ b/sturdy/pool_registry/pool_registry.py @@ -5,12 +5,10 @@ "pools": { "0x0669091F451142b3228171aE6aD794cF98288124": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x0669091F451142b3228171aE6aD794cF98288124", }, "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D", }, } @@ -22,46 +20,44 @@ "pools": { "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227", }, "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b", }, "0x26fe402A57D52c8a323bb6e09f06489C8216aC88": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x26fe402A57D52c8a323bb6e09f06489C8216aC88", }, "0x8dDE9A50a91cc0a5DaBdc5d3931c1AF60408c84D": { "pool_type": "STURDY_SILO", - "pool_model_disc": "CHAIN", "contract_address": "0x8dDE9A50a91cc0a5DaBdc5d3931c1AF60408c84D", }, } }, }, - "Morpho USDC Vaults": { - "user_address": "0x000000000000000000000000000000000000dEaD", + "Yearn DAI Vault": { "assets_and_pools": { "pools": { - "0xd63070114470f685b75B74D60EEc7c1113d33a3D": { - "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", - "contract_address": "0xd63070114470f685b75B74D60EEc7c1113d33a3D", + # "x83F20F44975D03b1b09e64809B757c47f942BEeA": { + # "pool_type": "SAVINGS_DAI", + # "contract_address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + # }, + "0x018008bfb33d285247A21d44E50697654f754e63": { + "pool_type": "AAVE_DEFAULT", + "contract_address": "0x018008bfb33d285247A21d44E50697654f754e63", + "user_address": "0xF0825750791A4444c5E70743270DcfA8Bb38f959" }, - "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB": { - "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", - "contract_address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", - }, - "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458": { - "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", - "contract_address": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458", + "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B": { + "pool_type": "AAVE_TARGET", + "contract_address": "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B", + "user_address": "0x1fd862499e9b9402de6c599b6c391f83981180ab" }, + # "0x6acEDA98725505737c0F00a3dA0d047304052948": { + # "pool_type": "YEARN", + # "contract_address": "0x6acEDA98725505737c0F00a3dA0d047304052948", + # }, } }, }, diff --git a/sturdy/pools.py b/sturdy/pools.py index 9ba3c85..dafd14a 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -44,11 +44,12 @@ class POOL_TYPES(IntEnum): STURDY_SILO = 1 - AAVE = 2 + AAVE_DEFAULT = 2 DAI_SAVINGS = 3 COMPOUND_V3 = 4 MORPHO = 5 YEARN_V3 = 6 + AAVE_TARGET = 7 def get_minimum_allocation(pool: "ChainBasedPoolModel") -> int: @@ -60,7 +61,7 @@ def get_minimum_allocation(pool: "ChainBasedPoolModel") -> int: borrow_amount = pool._totalBorrow our_supply = pool._curr_deposit_amount assets_available = pool._totalAssets - borrow_amount - case POOL_TYPES.AAVE: + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): # borrow amount for aave pools is total_stable_debt + total_variable_debt borrow_amount = ((pool._nextTotalStableDebt * int(1e18)) // int(10**pool._decimals)) + ( (pool._totalVariableDebt * int(1e18)) // int(10**pool._decimals) @@ -154,7 +155,6 @@ class Config: use_enum_values = True # This will use the enum's value instead of the enum itself smart_union = True - pool_model_disc: Literal["CHAIN"] = Field(default="CHAIN", description="pool type discriminator") pool_type: POOL_TYPES | int | str = Field(..., description="type of pool") user_address: str = Field( default=ADDRESS_ZERO, @@ -200,7 +200,7 @@ class PoolFactory: @staticmethod def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel: match pool_type: - case POOL_TYPES.AAVE: + case POOL_TYPES.AAVE_DEFAULT: return AaveV3DefaultInterestRatePool(**kwargs) case POOL_TYPES.STURDY_SILO: return VariableInterestSturdySiloStrategy(**kwargs) @@ -212,6 +212,8 @@ def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel: return MorphoVault(**kwargs) case POOL_TYPES.YEARN_V3: return YearnV3Vault(**kwargs) + case POOL_TYPES.AAVE_TARGET: + return AaveV3RateTargetBaseInterestRatePool(**kwargs) case _: raise ValueError(f"Unknown pool type: {pool_type}") @@ -219,7 +221,7 @@ def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel: class AaveV3DefaultInterestRatePool(ChainBasedPoolModel): """This class defines the default pool type for Aave""" - pool_type: POOL_TYPES = Field(default=POOL_TYPES.AAVE, const=True, description="type of pool") + pool_type: POOL_TYPES = Field(default=POOL_TYPES.AAVE_DEFAULT, const=True, description="type of pool") _atoken_contract: Contract = PrivateAttr() _pool_contract: Contract = PrivateAttr() @@ -235,6 +237,8 @@ class AaveV3DefaultInterestRatePool(ChainBasedPoolModel): _collateral_amount: int = PrivateAttr() _total_supplied: int = PrivateAttr() _decimals: int = PrivateAttr() + _user_asset_balance: int = PrivateAttr() + _normalized_income: int = PrivateAttr() class Config: arbitrary_types_allowed = True @@ -379,6 +383,225 @@ def sync(self, web3_provider: Web3) -> None: self._atoken_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call ) + self._user_asset_balance = retry_with_backoff( + self._underlying_asset_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call + ) + + self._normalized_income = retry_with_backoff( + self._pool_contract.functions.getReserveNormalizedIncome(self._underlying_asset_address).call + ) + + except Exception as err: + bt.logging.error("Failed to sync to chain!") + bt.logging.error(err) # type: ignore[] + + # last 256 unique calls to this will be cached for the next 60 seconds + @ttl_cache(maxsize=256, ttl=60) + def supply_rate(self, amount: int) -> int: + """Returns supply rate given new deposit amount""" + try: + already_deposited = self._collateral_amount + delta = amount - already_deposited + to_deposit = max(0, delta) + to_remove = abs(delta) if delta < 0 else 0 + + (nextLiquidityRate, _) = retry_with_backoff( + self._strategy_contract.functions.calculateInterestRates( + ( + self._reserve_data.unbacked, + int(to_deposit), + int(to_remove), + self._nextTotalStableDebt + self._totalVariableDebt, + self._reserveFactor, + self._underlying_asset_address, + True, + already_deposited, + ), + ).call, + ) + + return Web3.to_wei(nextLiquidityRate / 1e27, "ether") + + except Exception as e: + bt.logging.error("Failed to retrieve supply apy!") + bt.logging.error(e) # type: ignore[] + + return 0 + + +class AaveV3RateTargetBaseInterestRatePool(ChainBasedPoolModel): + """This class defines the default pool type for Aave""" + + pool_type: POOL_TYPES = Field(default=POOL_TYPES.AAVE_TARGET, const=True, description="type of pool") + + _atoken_contract: Contract = PrivateAttr() + _pool_contract: Contract = PrivateAttr() + _underlying_asset_contract: Contract = PrivateAttr() + _underlying_asset_address: str = PrivateAttr() + _reserve_data = PrivateAttr() + _strategy_contract = PrivateAttr() + _nextTotalStableDebt = PrivateAttr() + _nextAvgStableBorrowRate = PrivateAttr() + _variable_debt_token_contract = PrivateAttr() + _totalVariableDebt = PrivateAttr() + _reserveFactor = PrivateAttr() + _collateral_amount: int = PrivateAttr() + _total_supplied: int = PrivateAttr() + _decimals: int = PrivateAttr() + _user_asset_balance: int = PrivateAttr() + _normalized_income: int = PrivateAttr() + + class Config: + arbitrary_types_allowed = True + + def __hash__(self) -> int: + return hash((self._atoken_contract.address, self._underlying_asset_address)) + + def __eq__(self, other) -> bool: + if not isinstance(other, AaveV3DefaultInterestRatePool): + return NotImplemented + # Compare the attributes for equality + return (self._atoken_contract.address, self._underlying_asset_address) == ( + other._atoken_contract.address, + other._underlying_asset_address, + ) + + def pool_init(self, web3_provider: Web3) -> None: + try: + assert web3_provider.is_connected() + except Exception as err: + bt.logging.error("Failed to connect to Web3 instance!") + bt.logging.error(err) # type: ignore[] + + try: + atoken_abi_file_path = Path(__file__).parent / "abi/AToken.json" + atoken_abi_file = atoken_abi_file_path.open() + atoken_abi = json.load(atoken_abi_file) + atoken_abi_file.close() + atoken_contract = web3_provider.eth.contract(abi=atoken_abi, decode_tuples=True) + self._atoken_contract = retry_with_backoff( + atoken_contract, + address=self.contract_address, + ) + + pool_abi_file_path = Path(__file__).parent / "abi/Pool.json" + pool_abi_file = pool_abi_file_path.open() + pool_abi = json.load(pool_abi_file) + pool_abi_file.close() + + atoken_contract = self._atoken_contract + pool_address = retry_with_backoff(atoken_contract.functions.POOL().call) + + pool_contract = web3_provider.eth.contract(abi=pool_abi, decode_tuples=True) + self._pool_contract = retry_with_backoff(pool_contract, address=pool_address) + + self._underlying_asset_address = retry_with_backoff( + self._atoken_contract.functions.UNDERLYING_ASSET_ADDRESS().call, + ) + + erc20_abi_file_path = Path(__file__).parent / "abi/IERC20.json" + erc20_abi_file = erc20_abi_file_path.open() + erc20_abi = json.load(erc20_abi_file) + erc20_abi_file.close() + + underlying_asset_contract = web3_provider.eth.contract(abi=erc20_abi, decode_tuples=True) + self._underlying_asset_contract = retry_with_backoff( + underlying_asset_contract, + address=self._underlying_asset_address, + ) + + self._total_supplied = retry_with_backoff(self._atoken_contract.functions.totalSupply().call) + + self._initted = True + + except Exception as err: + bt.logging.error("Failed to load contract!") + bt.logging.error(err) # type: ignore[] + + def sync(self, web3_provider: Web3) -> None: + """Syncs with chain""" + if not self._initted: + self.pool_init(web3_provider) + try: + pool_abi_file_path = Path(__file__).parent / "abi/Pool.json" + pool_abi_file = pool_abi_file_path.open() + pool_abi = json.load(pool_abi_file) + pool_abi_file.close() + + atoken_contract_onchain = self._atoken_contract + pool_address = retry_with_backoff(atoken_contract_onchain.functions.POOL().call) + + pool_contract = web3_provider.eth.contract(abi=pool_abi, decode_tuples=True) + self._pool_contract = retry_with_backoff(pool_contract, address=pool_address) + + self._underlying_asset_address = retry_with_backoff( + self._atoken_contract.functions.UNDERLYING_ASSET_ADDRESS().call, + ) + + self._reserve_data = retry_with_backoff( + self._pool_contract.functions.getReserveData(self._underlying_asset_address).call, + ) + + reserve_strat_abi_file_path = Path(__file__).parent / "abi/RateTargetBaseInterestRateStrategy.json" + reserve_strat_abi_file = reserve_strat_abi_file_path.open() + reserve_strat_abi = json.load(reserve_strat_abi_file) + reserve_strat_abi_file.close() + + strategy_contract = web3_provider.eth.contract(abi=reserve_strat_abi) + self._strategy_contract = retry_with_backoff( + strategy_contract, + address=self._reserve_data.interestRateStrategyAddress, + ) + + stable_debt_token_abi_file_path = Path(__file__).parent / "abi/IStableDebtToken.json" + stable_debt_token_abi_file = stable_debt_token_abi_file_path.open() + stable_debt_token_abi = json.load(stable_debt_token_abi_file) + stable_debt_token_abi_file.close() + + stable_debt_token_contract = web3_provider.eth.contract(abi=stable_debt_token_abi) + stable_debt_token_contract = retry_with_backoff( + stable_debt_token_contract, + address=self._reserve_data.stableDebtTokenAddress, + ) + + ( + _, + self._nextTotalStableDebt, + self._nextAvgStableBorrowRate, + _, + ) = retry_with_backoff(stable_debt_token_contract.functions.getSupplyData().call) + + variable_debt_token_abi_file_path = Path(__file__).parent / "abi/IVariableDebtToken.json" + variable_debt_token_abi_file = variable_debt_token_abi_file_path.open() + variable_debt_token_abi = json.load(variable_debt_token_abi_file) + variable_debt_token_abi_file.close() + + variable_debt_token_contract = web3_provider.eth.contract(abi=variable_debt_token_abi) + self._variable_debt_token_contract = retry_with_backoff( + variable_debt_token_contract, + address=self._reserve_data.variableDebtTokenAddress, + ) + + nextVariableBorrowIndex = self._reserve_data.variableBorrowIndex + + nextScaledVariableDebt = retry_with_backoff(self._variable_debt_token_contract.functions.scaledTotalSupply().call) + self._totalVariableDebt = rayMul(nextScaledVariableDebt, nextVariableBorrowIndex) + + reserveConfiguration = self._reserve_data.configuration + self._reserveFactor = getReserveFactor(reserveConfiguration) + self._decimals = retry_with_backoff(self._underlying_asset_contract.functions.decimals().call) + self._collateral_amount = retry_with_backoff( + self._atoken_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call + ) + + self._user_asset_balance = retry_with_backoff( + self._underlying_asset_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call + ) + + self._normalized_income = retry_with_backoff( + self._pool_contract.functions.getReserveNormalizedIncome(self._underlying_asset_address).call + ) + except Exception as err: bt.logging.error("Failed to sync to chain!") bt.logging.error(err) # type: ignore[] @@ -892,6 +1115,44 @@ def supply_rate(self, amount: int) -> int: ) +class YearnV3Vault(ChainBasedPoolModel): + pool_type: POOL_TYPES = Field(POOL_TYPES.YEARN_V3, const=True, description="type of pool") + + _vault_contract: Contract = PrivateAttr() + _apr_oracle: Contract = PrivateAttr() + _max_withdraw: int = PrivateAttr() + _curr_deposit: int = PrivateAttr() + + def pool_init(self, web3_provider: Web3) -> None: + vault_abi_file_path = Path(__file__).parent / "abi/Yearn_V3_Vault.json" + vault_abi_file = vault_abi_file_path.open() + vault_abi = json.load(vault_abi_file) + vault_abi_file.close() + + vault_contract = web3_provider.eth.contract(abi=vault_abi, decode_tuples=True) + self._vault_contract = retry_with_backoff(vault_contract, address=self.contract_address) + + apr_oracle_abi_file_path = Path(__file__).parent / "abi/AprOracle.json" + apr_oracle_abi_file = apr_oracle_abi_file_path.open() + apr_oracle_abi = json.load(apr_oracle_abi_file) + apr_oracle_abi_file.close() + + apr_oracle = web3_provider.eth.contract(abi=apr_oracle_abi, decode_tuples=True) + self._apr_oracle = retry_with_backoff(apr_oracle, address=APR_ORACLE) + + def sync(self, web3_provider: Web3) -> None: + if not self._initted: + self.pool_init(web3_provider) + + self._max_withdraw = retry_with_backoff(self._vault_contract.functions.maxWithdraw(self.user_address).call) + user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) + self._curr_deposit = retry_with_backoff(self._vault_contract.functions.convertToAssets(user_shares).call) + + def supply_rate(self, amount: int) -> int: + delta = amount - self._curr_deposit + return retry_with_backoff(self._apr_oracle.functions.getExpectedApr(self.contract_address, delta).call) + + def generate_eth_public_key(rng_gen: np.random.RandomState) -> str: private_key_bytes = rng_gen.bytes(32) # type: ignore[] account = Account.from_key(private_key_bytes) @@ -915,15 +1176,15 @@ def assets_pools_for_challenge_data( selected_assets_and_pools = selected_entry["assets_and_pools"] selected_pools = selected_assets_and_pools["pools"] - user_address = selected_entry["user_address"] + global_user_address = selected_entry.get("user_address", None) pool_list = [] for pool_dict in selected_pools.values(): + user_address = pool_dict.get("user_address", None) pool = PoolFactory.create_pool( pool_type=POOL_TYPES._member_map_[pool_dict["pool_type"]], - pool_model_disc=pool_dict["pool_model_disc"], - user_address=user_address, + user_address=global_user_address if user_address is None else user_address, contract_address=pool_dict["contract_address"], ) pool_list.append(pool) @@ -935,9 +1196,9 @@ def assets_pools_for_challenge_data( first_pool = pool_list[0] total_assets = 0 + first_pool.sync(web3_provider) match first_pool.pool_type: - case POOL_TYPES.STURDY_SILO: - first_pool.sync(web3_provider) + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): total_assets = first_pool._user_asset_balance case _: pass @@ -948,6 +1209,8 @@ def assets_pools_for_challenge_data( match pool.pool_type: case POOL_TYPES.STURDY_SILO: total_asset += pool._curr_deposit_amount + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): + total_asset += pool._collateral_amount case _: pass @@ -956,57 +1219,7 @@ def assets_pools_for_challenge_data( challenge_data["assets_and_pools"] = {} challenge_data["assets_and_pools"]["pools"] = pools challenge_data["assets_and_pools"]["total_assets"] = total_assets - challenge_data["user_address"] = user_address + if global_user_address is not None: + challenge_data["user_address"] = global_user_address return challenge_data - - -class YearnV3Vault(ChainBasedPoolModel): - pool_type: POOL_TYPES = Field(POOL_TYPES.YEARN_V3, const=True, description="type of pool") - - _vault_contract: Contract = PrivateAttr() - _apr_oracle: Contract = PrivateAttr() - _max_withdraw: int = PrivateAttr() - _curr_deposit: int = PrivateAttr() - - def pool_init(self, web3_provider: Web3) -> None: - vault_abi_file_path = Path(__file__).parent / "abi/Yearn_V3_Vault.json" - vault_abi_file = vault_abi_file_path.open() - vault_abi = json.load(vault_abi_file) - vault_abi_file.close() - - vault_contract = web3_provider.eth.contract(abi=vault_abi, decode_tuples=True) - self._vault_contract = retry_with_backoff(vault_contract, address=self.contract_address) - - apr_oracle_abi_file_path = Path(__file__).parent / "abi/AprOracle.json" - apr_oracle_abi_file = apr_oracle_abi_file_path.open() - apr_oracle_abi = json.load(apr_oracle_abi_file) - apr_oracle_abi_file.close() - - apr_oracle = web3_provider.eth.contract(abi=apr_oracle_abi, decode_tuples=True) - self._apr_oracle = retry_with_backoff(apr_oracle, address=APR_ORACLE) - - def sync(self, web3_provider: Web3) -> None: - if not self._initted: - self.pool_init(web3_provider) - - self._max_withdraw = retry_with_backoff(self._vault_contract.functions.maxWithdraw(self.user_address).call) - user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) - self._curr_deposit = retry_with_backoff(self._vault_contract.functions.convertToAssets(user_shares).call) - - def supply_rate(self, amount: int) -> int: - delta = amount - self._curr_deposit - return retry_with_backoff(self._apr_oracle.functions.getExpectedApr(self.contract_address, delta).call) - - -# TODO: remove this? -# # generate intial allocations for pools -# def generate_initial_allocations_for_pools(assets_and_pools: dict) -> dict: -# total_assets: int = assets_and_pools["total_assets"] -# pools: dict[str, Chain] = assets_and_pools["pools"] -# allocs = {} -# for pool_uid, pool in pools.items(): -# alloc = pool.borrow_amount if pool.pool_type == POOL_TYPES.SYNTHETIC else total_assets // len(pools) -# allocs[pool_uid] = alloc - -# return allocs diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 881d315..db21115 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -26,7 +26,7 @@ from sturdy.constants import MAX_SCORING_PERIOD, MIN_SCORING_PERIOD, QUERY_TIMEOUT, SCORING_PERIOD_STEP from sturdy.pool_registry.pool_registry import POOL_REGISTRY -from sturdy.pools import assets_pools_for_challenge_data +from sturdy.pools import POOL_TYPES, assets_pools_for_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations @@ -44,16 +44,17 @@ async def forward(self) -> Any: """ # initialize pools and assets # TODO: only sturdy pools for now - selected_entry = POOL_REGISTRY["Sturdy Crvusd Aggregator"] + selected_entry = POOL_REGISTRY["Yearn DAI Vault"] challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) request_uuid = str(uuid.uuid4()).replace("-", "") + user_address = challenge_data.get("user_address", None) bt.logging.info("Querying miners...") axon_times, allocations = await query_and_score_miners( self, assets_and_pools=challenge_data["assets_and_pools"], request_type=REQUEST_TYPES.SYNTHETIC, - user_address=challenge_data["user_address"], + user_address=user_address if user_address is not None else ADDRESS_ZERO, ) assets_and_pools = challenge_data["assets_and_pools"] @@ -62,7 +63,13 @@ async def forward(self) -> Any: for contract_addr, pool in pools.items(): pool.sync(self.w3) - metadata[contract_addr] = pool._price_per_share + match pool.pool_type: + case POOL_TYPES.STURDY_SILO: + metadata[contract_addr] = pool._price_per_share + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): + metadata[contract_addr] = pool._normalized_income + case _: + pass scoring_period = get_scoring_period() diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 82734c3..e28a18c 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -277,6 +277,11 @@ def generated_yield_pct( curr_share_price = pool._price_per_share pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) total_yield += int(allocation * pct_delta) + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): + last_income = extra_metadata[contract_addr] + curr_income = pool._normalized_income + pct_delta = float(curr_income - last_income) / float(last_income) + total_yield += int(allocation * pct_delta) case _: total_yield += 0 diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index b8499c6..48d8027 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -22,7 +22,7 @@ os.environ["WEB_PROVIDER_URL"] = "http://127.0.0.1:8545" -# TODO: more comprehensive integration testing? +# TODO: more comprehensive integration testing - with in-mem sql db and everythin' class TestValidator(IsolatedAsyncioTestCase): maxDiff = 4000 diff --git a/tests/unit/validator/test_pool_generator.py b/tests/unit/validator/test_pool_generator.py index 6811a5f..fcfa695 100644 --- a/tests/unit/validator/test_pool_generator.py +++ b/tests/unit/validator/test_pool_generator.py @@ -29,7 +29,7 @@ def setUpClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 21080765, + "blockNumber": 21150770, }, }, ], @@ -60,7 +60,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 21080765, + "blockNumber": 21150770, }, }, ], @@ -80,9 +80,10 @@ def test_generate_assets_and_pools(self) -> None: np.random.seed(69) # run test multiple times to to ensure the number generated are # within the correct ranges - keys = list(POOL_REGISTRY.keys())[:2] - for idx in range(2): + keys = list(POOL_REGISTRY.keys()) + for idx in range(len(keys)): key = keys[idx] + print(key) selected_entry = POOL_REGISTRY[key] generated = assets_pools_for_challenge_data(selected_entry, self.w3) print(generated) diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index d93f451..f43e223 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -11,6 +11,7 @@ from sturdy.constants import APR_ORACLE from sturdy.pools import ( AaveV3DefaultInterestRatePool, + AaveV3RateTargetBaseInterestRatePool, CompoundV3Pool, DaiSavingsRate, MorphoVault, @@ -124,6 +125,11 @@ def test_sync(self) -> None: self.assertTrue(hasattr(pool, "_pool_contract")) self.assertTrue(isinstance(pool._pool_contract, Contract)) + self.assertTrue(hasattr(pool, "_normalized_income")) + self.assertTrue(isinstance(pool._normalized_income, int)) + self.assertGreaterEqual(pool._normalized_income, int(1e27)) + print(f"normalized income: {pool._normalized_income}") + # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: print("----==== test_supply_rate_increase_alloc ====----") @@ -815,5 +821,211 @@ def test_supply_rate_decrease_alloc(self) -> None: self.assertGreater(apy_after, apy_before) +# TODO: make testaavepool and this test use the same block number but different address +# right now they both use the same pool but from different blocks in the past. +class TestAaveTargetPool(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + # runs tests on local mainnet fork at block: 20233401 + cls.w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + assert cls.w3.is_connected() + + cls.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21150770, + }, + }, + ], + ) + + cls.atoken_address = "0x4d5F47FA6A74757f35C14fD3a6Ef8E3C9BC514E8" + # Create a funded account for testing + cls.account = Account.create() + cls.w3.eth.send_transaction( + { + "to": cls.account.address, + "from": cls.w3.eth.accounts[0], + "value": cls.w3.to_wei(200000, "ether"), + } + ) + + weth_abi_file_path = Path(__file__).parent / "../../../sturdy/abi/IWETH.json" + weth_abi_file = weth_abi_file_path.open() + weth_abi = json.load(weth_abi_file) + weth_abi_file.close() + + weth_contract = cls.w3.eth.contract(abi=weth_abi) + cls.weth_contract = retry_with_backoff( + weth_contract, + address=Web3.to_checksum_address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), + ) + + cls.snapshot_id = cls.w3.provider.make_request("evm_snapshot", []) # type: ignore[] + print(f"snapshot id: {cls.snapshot_id}") + + @classmethod + def tearDownClass(cls) -> None: + # run this after tests to restore original forked state + w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + + w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21150770, + }, + }, + ], + ) + + def setUp(self) -> None: + self.snapshot_id = self.w3.provider.make_request("evm_snapshot", []) # type: ignore[] + print(f"snapshot id: {self.snapshot_id}") + + def tearDown(self) -> None: + # Optional: Revert to the original snapshot after each test + print("reverting to original evm snapshot") + self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] + + def test_pool_contract(self) -> None: + print("----==== test_pool_contract ====----") + # we call the aave3 weth atoken proxy contract in this example + pool = AaveV3DefaultInterestRatePool( + contract_address=self.atoken_address, + ) + + pool.pool_init(self.w3) + self.assertTrue(hasattr(pool, "_atoken_contract")) + self.assertTrue(isinstance(pool._atoken_contract, Contract)) + + self.assertTrue(hasattr(pool, "_pool_contract")) + self.assertTrue(isinstance(pool._pool_contract, Contract)) + + # TODO: test syncing after time travel + def test_sync(self) -> None: + print("----==== test_sync ====----") + pool = AaveV3DefaultInterestRatePool( + contract_address=self.atoken_address, + ) + + # sync pool params + pool.sync(web3_provider=self.w3) + + self.assertTrue(hasattr(pool, "_atoken_contract")) + self.assertTrue(isinstance(pool._atoken_contract, Contract)) + + self.assertTrue(hasattr(pool, "_pool_contract")) + self.assertTrue(isinstance(pool._pool_contract, Contract)) + + self.assertTrue(hasattr(pool, "_normalized_income")) + self.assertTrue(isinstance(pool._normalized_income, int)) + self.assertGreaterEqual(pool._normalized_income, int(1e27)) + print(f"normalized income: {pool._normalized_income}") + + # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests + def test_supply_rate_alloc(self) -> None: + print("----==== test_supply_rate_increase_alloc ====----") + pool = AaveV3DefaultInterestRatePool( + contract_address=self.atoken_address, + user_address=self.account.address + ) + + # sync pool params + pool.sync(web3_provider=self.w3) + + reserve_data = retry_with_backoff(pool._pool_contract.functions.getReserveData(pool._underlying_asset_address).call) + + apy_before = Web3.to_wei(reserve_data.currentLiquidityRate / 1e27, "ether") + print(f"apy before supplying: {apy_before}") + + # calculate predicted future supply rate after supplying 10000 ETH + apy_after = pool.supply_rate(int(1000e18)) + print(f"apy after supplying 10000 ETH: {apy_after}") + self.assertNotEqual(apy_after, 0) + self.assertLess(apy_after, apy_before) + + def test_supply_rate_decrease_alloc(self) -> None: + print("----==== test_supply_rate_decrease_alloc ====----") + pool = AaveV3RateTargetBaseInterestRatePool(contract_address=self.atoken_address, user_address=self.account.address) + + # sync pool params + pool.sync(web3_provider=self.w3) + + tx = self.weth_contract.functions.deposit().build_transaction( + { + "from": self.w3.to_checksum_address(self.account.address), + "gas": 100000, + "gasPrice": self.w3.eth.gas_price, + "nonce": self.w3.eth.get_transaction_count(self.account.address), + "value": self.w3.to_wei(10000, "ether"), + } + ) + + signed_tx = self.w3.eth.account.sign_transaction(transaction_dict=tx, private_key=self.account.key) + + # Send the transaction + tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + print(f"weth deposit tx hash: {tx_hash}") + + # check if we received some weth + weth_balance = self.weth_contract.functions.balanceOf(self.account.address).call() + self.assertGreaterEqual(int(weth_balance), self.w3.to_wei(10000, "ether")) + + # approve aave pool to use weth + tx = self.weth_contract.functions.approve(pool._pool_contract.address, self.w3.to_wei(1e9, "ether")).build_transaction( + { + "from": self.w3.to_checksum_address(self.account.address), + "gas": 1000000, + "gasPrice": self.w3.eth.gas_price, + "nonce": self.w3.eth.get_transaction_count(self.account.address), + } + ) + + signed_tx = self.w3.eth.account.sign_transaction(transaction_dict=tx, private_key=self.account.key) + + # Send the transaction + tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + print(f"pool approve weth tx hash: {tx_hash}") + + # deposit tokens into the pool + tx = pool._pool_contract.functions.supply( + self.weth_contract.address, + self.w3.to_wei(10000, "ether"), + self.account.address, + 0, + ).build_transaction( + { + "from": self.w3.to_checksum_address(self.account.address), + "gas": 1000000, + "gasPrice": self.w3.eth.gas_price, + "nonce": self.w3.eth.get_transaction_count(self.account.address), + } + ) + + signed_tx = self.w3.eth.account.sign_transaction(transaction_dict=tx, private_key=self.account.key) + + # Send the transaction + tx_hash = retry_with_backoff(self.w3.eth.send_raw_transaction, signed_tx.rawTransaction) + print(f"supply weth tx hash: {tx_hash}") + + reserve_data = retry_with_backoff(pool._pool_contract.functions.getReserveData(pool._underlying_asset_address).call) + + apy_before = Web3.to_wei(reserve_data.currentLiquidityRate / 1e27, "ether") + print(f"apy before rebalancing ether: {apy_before}") + + # calculate predicted future supply rate after removing 1000 ETH to end up with 9000 ETH in the pool + pool.sync(self.w3) + apy_after = pool.supply_rate(int(9000e18)) + print(f"apy after rebalancing ether: {apy_after}") + self.assertNotEqual(apy_after, 0) + self.assertGreater(apy_after, apy_before) + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 8348c3c..24f57f7 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -803,6 +803,75 @@ def test_calculate_apy_sturdy(self) -> None: print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") self.assertGreater(apy, 0) + def test_calculate_apy_aave(self) -> None: + self.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21075005, + }, + }, + ], + ) + + # aave pools - with yearn strategies being their users + selected_entry = { + "assets_and_pools": { + "pools": { + "0x018008bfb33d285247A21d44E50697654f754e63": { + "pool_type": "AAVE_DEFAULT", + "contract_address": "0x018008bfb33d285247A21d44E50697654f754e63", + "user_address": "0xF0825750791A4444c5E70743270DcfA8Bb38f959", + }, + "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B": { + "pool_type": "AAVE_TARGET", + "contract_address": "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B", + "user_address": "0x1fd862499e9b9402de6c599b6c391f83981180ab", + }, + } + } + } + + selected = assets_pools_for_challenge_data(selected_entry, self.w3) + + assets_and_pools = selected["assets_and_pools"] + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=assets_and_pools, + ) + + allocations = naive_algorithm(self, synapse) + + extra_metadata = {} + for contract_address, pool in assets_and_pools["pools"].items(): + pool.sync(self.w3) + extra_metadata[contract_address] = pool._normalized_income + + # move forwards in time, back to "present" + # TODO: why doesnt the following work? + # self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] + + self.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + for pool in assets_and_pools["pools"].values(): + pool.sync(self.w3) + + apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) + print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") + self.assertGreater(apy, 0) + if __name__ == "__main__": unittest.main() From 3d8399109b7763500e59022fdb4e974a380e392f Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 12 Nov 2024 01:40:25 +0000 Subject: [PATCH 19/51] feat: starting to add back morpho vault support --- sturdy/pool_registry/pool_registry.py | 51 +++++++++++++----------- sturdy/pools.py | 36 +++++++++-------- sturdy/validator/forward.py | 9 +++-- tests/unit/validator/test_pool_models.py | 7 ++++ 4 files changed, 58 insertions(+), 45 deletions(-) diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py index 2e6824b..8de8737 100644 --- a/sturdy/pool_registry/pool_registry.py +++ b/sturdy/pool_registry/pool_registry.py @@ -37,28 +37,31 @@ } }, }, - "Yearn DAI Vault": { - "assets_and_pools": { - "pools": { - # "x83F20F44975D03b1b09e64809B757c47f942BEeA": { - # "pool_type": "SAVINGS_DAI", - # "contract_address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", - # }, - "0x018008bfb33d285247A21d44E50697654f754e63": { - "pool_type": "AAVE_DEFAULT", - "contract_address": "0x018008bfb33d285247A21d44E50697654f754e63", - "user_address": "0xF0825750791A4444c5E70743270DcfA8Bb38f959" - }, - "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B": { - "pool_type": "AAVE_TARGET", - "contract_address": "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B", - "user_address": "0x1fd862499e9b9402de6c599b6c391f83981180ab" - }, - # "0x6acEDA98725505737c0F00a3dA0d047304052948": { - # "pool_type": "YEARN", - # "contract_address": "0x6acEDA98725505737c0F00a3dA0d047304052948", - # }, - } - }, - }, + # "Morpho USDC Vaults": { + # "user_address": "0xFA60E843a52eff94901f43ac08232b59351192cc", + # "assets_and_pools": { + # "pools": { + # "0xd63070114470f685b75B74D60EEc7c1113d33a3D": { + # "pool_type": "MORPHO", + # "pool_model_disc": "CHAIN", + # "contract_address": "0xd63070114470f685b75B74D60EEc7c1113d33a3D", + # }, + # "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB": { + # "pool_type": "MORPHO", + # "pool_model_disc": "CHAIN", + # "contract_address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", + # }, + # "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458": { + # "pool_type": "MORPHO", + # "pool_model_disc": "CHAIN", + # "contract_address": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458", + # }, + # "0xdd0f28e19C1780eb6396170735D45153D261490d": { + # "pool_type": "MORPHO", + # "pool_model_disc": "CHAIN", + # "contract_address": "0xdd0f28e19C1780eb6396170735D45153D261490d", + # }, + # } + # }, + # }, } diff --git a/sturdy/pools.py b/sturdy/pools.py index dafd14a..e2988eb 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -950,23 +950,7 @@ def supply_rate(self) -> int: class MorphoVault(ChainBasedPoolModel): - # TODO: remove - """Model for Morpho Vaults - NOTE: - This pool type is a bit different from other pools - pools = { - ... - "0x0...1": { # <--- this is the index of the market in the morpho vault's "supplyQueue" - "contract_address": "0x....", # <---- this is the address of the morpho vault - ... - } - ... - } - - new_pools = [] - for pool_uid, pool in pools.items(): - new_pool = MorphoVault(contract_address=pool["contract_address"], market_idx=pool_uid ...) - """ + """Model for Morpho Vaults""" pool_type: POOL_TYPES = Field(POOL_TYPES.MORPHO, const=True, description="type of pool") @@ -981,6 +965,8 @@ class MorphoVault(ChainBasedPoolModel): _user_assets: int = PrivateAttr() _curr_borrows: int = PrivateAttr() _asset_decimals: int = PrivateAttr() + _underlying_asset_contract: Contract = PrivateAttr() + _user_asset_balance: int = PrivateAttr() _VIRTUAL_SHARES: ClassVar[int] = 1e6 _VIRTUAL_ASSETS: ClassVar[int] = 1 @@ -1022,6 +1008,21 @@ def pool_init(self, web3_provider: Web3) -> None: self._irm_abi = json.load(irm_abi_file) irm_abi_file.close() + underlying_asset_address = retry_with_backoff( + self._vault_contract.functions.asset().call + ) + + erc20_abi_file_path = Path(__file__).parent / "abi/IERC20.json" + erc20_abi_file = erc20_abi_file_path.open() + erc20_abi = json.load(erc20_abi_file) + erc20_abi_file.close() + + underlying_asset_contract = web3_provider.eth.contract(abi=erc20_abi, decode_tuples=True) + self._underlying_asset_contract = retry_with_backoff( + underlying_asset_contract, + address=underlying_asset_address, + ) + self._initted = True def sync(self, web3_provider: Web3) -> None: @@ -1050,6 +1051,7 @@ def sync(self, web3_provider: Web3) -> None: self._total_assets = retry_with_backoff(self._vault_contract.functions.totalAssets().call) curr_user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) self._user_assets = retry_with_backoff(self._vault_contract.functions.convertToAssets(curr_user_shares).call) + self._user_asset_balance = retry_with_backoff(self._underlying_asset_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call) self._curr_borrows = total_borrows @classmethod diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index db21115..cf6b8c6 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -26,7 +26,7 @@ from sturdy.constants import MAX_SCORING_PERIOD, MIN_SCORING_PERIOD, QUERY_TIMEOUT, SCORING_PERIOD_STEP from sturdy.pool_registry.pool_registry import POOL_REGISTRY -from sturdy.pools import POOL_TYPES, assets_pools_for_challenge_data +from sturdy.pools import POOL_TYPES, assets_pools_for_challenge_data, generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations @@ -43,9 +43,10 @@ async def forward(self) -> Any: """ # initialize pools and assets - # TODO: only sturdy pools for now - selected_entry = POOL_REGISTRY["Yearn DAI Vault"] - challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) + # TODO: only specific pools for now + # selected_entry = POOL_REGISTRY["Morpho USDC Vaults"] + # challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) + challenge_data = generate_challenge_data(self.w3) request_uuid = str(uuid.uuid4()).replace("-", "") user_address = challenge_data.get("user_address", None) diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index f43e223..c255d79 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -635,6 +635,13 @@ def test_morphovault_pool_model(self) -> None: self.assertTrue(hasattr(pool, "_curr_borrows")) self.assertTrue(isinstance(pool._curr_borrows, int)) + self.assertTrue(hasattr(pool, "_underlying_asset_contract")) + self.assertTrue(isinstance(pool._underlying_asset_contract, Contract)) + self.assertTrue(hasattr(pool, "_user_asset_balance")) + self.assertTrue(isinstance(pool._user_asset_balance, int)) + print(f"user asset balance: {pool._user_asset_balance}") + self.assertGreater(pool._user_asset_balance, 0) + # check pool supply_rate print(pool.supply_rate(0)) From 527d994552c0c3a28fd72ddf59fe3e050b72f823 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 12 Nov 2024 12:38:54 +0000 Subject: [PATCH 20/51] feat: morpho vaults --- neurons/validator.py | 2 +- sturdy/pool_registry/pool_registry.py | 55 ++++++++--------- sturdy/pools.py | 67 ++++++++++++--------- sturdy/validator/forward.py | 8 +-- sturdy/validator/reward.py | 8 ++- tests/unit/validator/test_pool_models.py | 17 ++++-- tests/unit/validator/test_reward_helpers.py | 52 +++++++++++++++- 7 files changed, 136 insertions(+), 73 deletions(-) diff --git a/neurons/validator.py b/neurons/validator.py index fee5845..eee2a58 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -244,7 +244,7 @@ async def allocate(body: AllocateAssetsRequest) -> AllocateAssetsResponse | None for contract_addr, pool in pools.items(): pool.sync(core_validator.w3) - metadata[contract_addr] = pool._price_per_share + metadata[contract_addr] = pool._share_price with sql.get_db_connection() as conn: sql.log_allocations( diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py index 8de8737..e9c4fc4 100644 --- a/sturdy/pool_registry/pool_registry.py +++ b/sturdy/pool_registry/pool_registry.py @@ -37,31 +37,32 @@ } }, }, - # "Morpho USDC Vaults": { - # "user_address": "0xFA60E843a52eff94901f43ac08232b59351192cc", - # "assets_and_pools": { - # "pools": { - # "0xd63070114470f685b75B74D60EEc7c1113d33a3D": { - # "pool_type": "MORPHO", - # "pool_model_disc": "CHAIN", - # "contract_address": "0xd63070114470f685b75B74D60EEc7c1113d33a3D", - # }, - # "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB": { - # "pool_type": "MORPHO", - # "pool_model_disc": "CHAIN", - # "contract_address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", - # }, - # "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458": { - # "pool_type": "MORPHO", - # "pool_model_disc": "CHAIN", - # "contract_address": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458", - # }, - # "0xdd0f28e19C1780eb6396170735D45153D261490d": { - # "pool_type": "MORPHO", - # "pool_model_disc": "CHAIN", - # "contract_address": "0xdd0f28e19C1780eb6396170735D45153D261490d", - # }, - # } - # }, - # }, + "Morpho USDC Vaults": { + "user_address": "0xFA60E843a52eff94901f43ac08232b59351192cc", + "total_assets": 1000000000000, + "assets_and_pools": { + "pools": { + "0xd63070114470f685b75B74D60EEc7c1113d33a3D": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0xd63070114470f685b75B74D60EEc7c1113d33a3D", + }, + "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", + }, + "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458", + }, + "0xdd0f28e19C1780eb6396170735D45153D261490d": { + "pool_type": "MORPHO", + "pool_model_disc": "CHAIN", + "contract_address": "0xdd0f28e19C1780eb6396170735D45153D261490d", + }, + } + }, + }, } diff --git a/sturdy/pools.py b/sturdy/pools.py index e2988eb..178a558 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -60,24 +60,24 @@ def get_minimum_allocation(pool: "ChainBasedPoolModel") -> int: case POOL_TYPES.STURDY_SILO: borrow_amount = pool._totalBorrow our_supply = pool._curr_deposit_amount - assets_available = pool._totalAssets - borrow_amount + assets_available = max(0, pool._totalAssets - borrow_amount) case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): # borrow amount for aave pools is total_stable_debt + total_variable_debt borrow_amount = ((pool._nextTotalStableDebt * int(1e18)) // int(10**pool._decimals)) + ( (pool._totalVariableDebt * int(1e18)) // int(10**pool._decimals) ) our_supply = pool._collateral_amount - assets_available = ((pool._total_supplied * int(1e18)) // int(10**pool._decimals)) - borrow_amount + assets_available = max(0, ((pool._total_supplied * int(1e18)) // int(10**pool._decimals)) - borrow_amount) case POOL_TYPES.COMPOUND_V3: borrow_amount = pool._total_borrow our_supply = pool._deposit_amount - assets_available = pool._total_supply - borrow_amount + assets_available = max(0, pool._total_supply - borrow_amount) case POOL_TYPES.MORPHO: borrow_amount = pool._curr_borrows our_supply = pool._user_assets - assets_available = pool._total_assets - borrow_amount + assets_available = max(0, pool._total_assets - borrow_amount) case POOL_TYPES.YEARN_V3: - return pool._curr_deposit - pool._max_withdraw + return max(0, pool._curr_deposit - pool._max_withdraw) case POOL_TYPES.DAI_SAVINGS: pass # TODO: is there a more appropriate way to go about this? case _: # not a valid pool type @@ -662,7 +662,7 @@ class VariableInterestSturdySiloStrategy(ChainBasedPoolModel): _asset: Contract = PrivateAttr() _user_asset_balance: int = PrivateAttr() _user_total_assets: int = PrivateAttr() - _price_per_share: Contract = PrivateAttr() + _share_price: Contract = PrivateAttr() def __hash__(self) -> int: return hash((self._silo_strategy_contract.address, self._pair_contract)) @@ -748,7 +748,7 @@ def sync(self, web3_provider: Web3) -> None: self._user_asset_balance = retry_with_backoff(self._asset.functions.balanceOf(self.user_address).call) # get current price per share - self._price_per_share = retry_with_backoff(self._pair_contract.functions.pricePerShare().call) + self._share_price = retry_with_backoff(self._pair_contract.functions.pricePerShare().call) # last 256 unique calls to this will be cached for the next 60 seconds @ttl_cache(maxsize=256, ttl=60) @@ -967,6 +967,7 @@ class MorphoVault(ChainBasedPoolModel): _asset_decimals: int = PrivateAttr() _underlying_asset_contract: Contract = PrivateAttr() _user_asset_balance: int = PrivateAttr() + _share_price: int = PrivateAttr() _VIRTUAL_SHARES: ClassVar[int] = 1e6 _VIRTUAL_ASSETS: ClassVar[int] = 1 @@ -1008,9 +1009,7 @@ def pool_init(self, web3_provider: Web3) -> None: self._irm_abi = json.load(irm_abi_file) irm_abi_file.close() - underlying_asset_address = retry_with_backoff( - self._vault_contract.functions.asset().call - ) + underlying_asset_address = retry_with_backoff(self._vault_contract.functions.asset().call) erc20_abi_file_path = Path(__file__).parent / "abi/IERC20.json" erc20_abi_file = erc20_abi_file_path.open() @@ -1051,9 +1050,13 @@ def sync(self, web3_provider: Web3) -> None: self._total_assets = retry_with_backoff(self._vault_contract.functions.totalAssets().call) curr_user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) self._user_assets = retry_with_backoff(self._vault_contract.functions.convertToAssets(curr_user_shares).call) - self._user_asset_balance = retry_with_backoff(self._underlying_asset_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call) + self._user_asset_balance = retry_with_backoff( + self._underlying_asset_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call + ) self._curr_borrows = total_borrows + self._share_price = retry_with_backoff(self._vault_contract.functions.convertToAssets(int(1e18)).call) + @classmethod def assets_to_shares_down(cls, assets: int, total_assets: int, total_shares: int) -> int: return (assets * (total_shares + cls._VIRTUAL_SHARES)) // (total_assets + cls._VIRTUAL_ASSETS) @@ -1195,28 +1198,32 @@ def assets_pools_for_challenge_data( # we assume that the user address is the same across pools (valid) # and also that the asset contracts are the same across said pools - first_pool = pool_list[0] - total_assets = 0 - - first_pool.sync(web3_provider) - match first_pool.pool_type: - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - total_assets = first_pool._user_asset_balance - case _: - pass - - for pool in pools.values(): - pool.sync(web3_provider) - total_asset = 0 - match pool.pool_type: - case POOL_TYPES.STURDY_SILO: - total_asset += pool._curr_deposit_amount - case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - total_asset += pool._collateral_amount + total_assets = selected_entry.get("total_assets", None) + + if total_assets is None: + total_assets = 0 + first_pool = pool_list[0] + first_pool.sync(web3_provider) + match first_pool.pool_type: + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET, POOL_TYPES.MORPHO): + total_assets = first_pool._user_asset_balance case _: pass - total_assets += total_asset + for pool in pools.values(): + pool.sync(web3_provider) + total_asset = 0 + match pool.pool_type: + case POOL_TYPES.STURDY_SILO: + total_asset += pool._curr_deposit_amount + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): + total_asset += pool._collateral_amount + case POOL_TYPES.MORPHO: + total_asset += pool._user_assets + case _: + pass + + total_assets += total_asset challenge_data["assets_and_pools"] = {} challenge_data["assets_and_pools"]["pools"] = pools diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index cf6b8c6..03f9568 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -43,9 +43,7 @@ async def forward(self) -> Any: """ # initialize pools and assets - # TODO: only specific pools for now - # selected_entry = POOL_REGISTRY["Morpho USDC Vaults"] - # challenge_data = assets_pools_for_challenge_data(selected_entry, self.w3) + # TODO: only sturdy silos and morpho vaults for now challenge_data = generate_challenge_data(self.w3) request_uuid = str(uuid.uuid4()).replace("-", "") user_address = challenge_data.get("user_address", None) @@ -65,8 +63,8 @@ async def forward(self) -> Any: for contract_addr, pool in pools.items(): pool.sync(self.w3) match pool.pool_type: - case POOL_TYPES.STURDY_SILO: - metadata[contract_addr] = pool._price_per_share + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): + metadata[contract_addr] = pool._share_price case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): metadata[contract_addr] = pool._normalized_income case _: diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index e28a18c..7ecdbb1 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -261,7 +261,7 @@ def generated_yield_pct( allocations: AllocationsDict, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], extra_metadata: dict ) -> int: """ - Calculates immediate projected yields given intial assets and pools, pool history, and number of timesteps + Calculates yields generated allocations in pools within scoring period """ # calculate projected yield @@ -272,9 +272,9 @@ def generated_yield_pct( for contract_addr, pool in pools.items(): allocation = allocations[contract_addr] match pool.pool_type: - case POOL_TYPES.STURDY_SILO: + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): last_share_price = extra_metadata[contract_addr] - curr_share_price = pool._price_per_share + curr_share_price = pool._share_price pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) total_yield += int(allocation * pct_delta) case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): @@ -403,6 +403,8 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: axon_times[miner_uid] = miner_axon_time apys_and_allocations[miner_uid] = {"apy": miner_apy, "allocations": allocations} + print(f"yields and allocs: {apys_and_allocations}") + # TODO: there may be a better way to go about this if len(miner_uids) < 1: return ([], {}) diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index c255d79..3497d21 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -111,7 +111,7 @@ def test_pool_contract(self) -> None: # TODO: test syncing after time travel def test_sync(self) -> None: - print("----==== test_sync ====----") + print("----==== TestAavePool | test_sync ====----") pool = AaveV3DefaultInterestRatePool( contract_address=self.atoken_address, ) @@ -132,7 +132,7 @@ def test_sync(self) -> None: # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: - print("----==== test_supply_rate_increase_alloc ====----") + print("----==== TestAavePool | test_supply_rate_increase_alloc ====----") pool = AaveV3DefaultInterestRatePool( contract_address=self.atoken_address, ) @@ -152,7 +152,7 @@ def test_supply_rate_alloc(self) -> None: self.assertLess(apy_after, apy_before) def test_supply_rate_decrease_alloc(self) -> None: - print("----==== test_supply_rate_decrease_alloc ====----") + print("----==== TestAavePool | test_supply_rate_decrease_alloc ====----") pool = AaveV3DefaultInterestRatePool(contract_address=self.atoken_address, user_address=self.account.address) # sync pool params @@ -308,9 +308,9 @@ def test_silo_strategy_contract(self) -> None: self.assertTrue(isinstance(pool._rate_model_contract, Contract)) print(f"rate model contract: {pool._rate_model_contract.address}") - self.assertTrue(hasattr(pool, "_price_per_share")) - self.assertTrue(isinstance(pool._price_per_share, int)) - print(f"price per share: {pool._price_per_share}") + self.assertTrue(hasattr(pool, "_share_price")) + self.assertTrue(isinstance(pool._share_price, int)) + print(f"price per share: {pool._share_price}") # don't change deposit amount to pool by much prev_supply_rate = pool.supply_rate(int(630e18)) @@ -642,6 +642,11 @@ def test_morphovault_pool_model(self) -> None: print(f"user asset balance: {pool._user_asset_balance}") self.assertGreater(pool._user_asset_balance, 0) + self.assertTrue(hasattr(pool, "_share_price")) + self.assertTrue(isinstance(pool._share_price, int)) + print(f"morpho vault share price: {pool._share_price}") + self.assertGreater(pool._share_price, 0) + # check pool supply_rate print(pool.supply_rate(0)) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 24f57f7..a328340 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -778,7 +778,7 @@ def test_calculate_apy_sturdy(self) -> None: extra_metadata = {} for contract_address, pool in assets_and_pools["pools"].items(): pool.sync(self.w3) - extra_metadata[contract_address] = pool._price_per_share + extra_metadata[contract_address] = pool._share_price # move forwards in time, back to "present" # TODO: why doesnt the following work? @@ -872,6 +872,56 @@ def test_calculate_apy_aave(self) -> None: print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") self.assertGreater(apy, 0) + def test_calculate_apy_morpho(self) -> None: + self.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21075005, + }, + }, + ], + ) + + selected_entry = POOL_REGISTRY["Morpho USDC Vaults"] + selected = assets_pools_for_challenge_data(selected_entry, self.w3) + + assets_and_pools = selected["assets_and_pools"] + user_address = selected["user_address"] + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=assets_and_pools, + user_address=user_address, + ) + + allocations = naive_algorithm(self, synapse) + + extra_metadata = {} + for contract_address, pool in assets_and_pools["pools"].items(): + pool.sync(self.w3) + extra_metadata[contract_address] = pool._share_price + + self.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": WEB3_PROVIDER_URL, + "blockNumber": 21080765, + }, + }, + ], + ) + + for pool in assets_and_pools["pools"].values(): + pool.sync(self.w3) + + apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) + print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") + self.assertGreater(apy, 0) + if __name__ == "__main__": unittest.main() From 2994deffaf6fff2011444b1d0fc1e4b7ad1007fa Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 12 Nov 2024 23:04:04 +0000 Subject: [PATCH 21/51] chore: renaming aave stuff --- sturdy/pools.py | 8 ++++---- tests/unit/validator/test_pool_models.py | 20 ++++++++++---------- tests/unit/validator/test_reward_helpers.py | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/sturdy/pools.py b/sturdy/pools.py index 178a558..d00afd6 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -201,7 +201,7 @@ class PoolFactory: def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel: match pool_type: case POOL_TYPES.AAVE_DEFAULT: - return AaveV3DefaultInterestRatePool(**kwargs) + return AaveV3DefaultInterestRateV2Pool(**kwargs) case POOL_TYPES.STURDY_SILO: return VariableInterestSturdySiloStrategy(**kwargs) case POOL_TYPES.DAI_SAVINGS: @@ -218,7 +218,7 @@ def create_pool(pool_type: POOL_TYPES, **kwargs: Any) -> ChainBasedPoolModel: raise ValueError(f"Unknown pool type: {pool_type}") -class AaveV3DefaultInterestRatePool(ChainBasedPoolModel): +class AaveV3DefaultInterestRateV2Pool(ChainBasedPoolModel): """This class defines the default pool type for Aave""" pool_type: POOL_TYPES = Field(default=POOL_TYPES.AAVE_DEFAULT, const=True, description="type of pool") @@ -247,7 +247,7 @@ def __hash__(self) -> int: return hash((self._atoken_contract.address, self._underlying_asset_address)) def __eq__(self, other) -> bool: - if not isinstance(other, AaveV3DefaultInterestRatePool): + if not isinstance(other, AaveV3DefaultInterestRateV2Pool): return NotImplemented # Compare the attributes for equality return (self._atoken_contract.address, self._underlying_asset_address) == ( @@ -458,7 +458,7 @@ def __hash__(self) -> int: return hash((self._atoken_contract.address, self._underlying_asset_address)) def __eq__(self, other) -> bool: - if not isinstance(other, AaveV3DefaultInterestRatePool): + if not isinstance(other, AaveV3DefaultInterestRateV2Pool): return NotImplemented # Compare the attributes for equality return (self._atoken_contract.address, self._underlying_asset_address) == ( diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 3497d21..8f74ed6 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -10,7 +10,7 @@ from sturdy.constants import APR_ORACLE from sturdy.pools import ( - AaveV3DefaultInterestRatePool, + AaveV3DefaultInterestRateV2Pool, AaveV3RateTargetBaseInterestRatePool, CompoundV3Pool, DaiSavingsRate, @@ -38,7 +38,7 @@ def setUpClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 20233401, + "blockNumber": 21150770, }, }, ], @@ -80,7 +80,7 @@ def tearDownClass(cls) -> None: { "forking": { "jsonRpcUrl": WEB3_PROVIDER_URL, - "blockNumber": 21080765, + "blockNumber": 21150770, }, }, ], @@ -98,7 +98,7 @@ def tearDown(self) -> None: def test_pool_contract(self) -> None: print("----==== test_pool_contract ====----") # we call the aave3 weth atoken proxy contract in this example - pool = AaveV3DefaultInterestRatePool( + pool = AaveV3RateTargetBaseInterestRatePool( contract_address=self.atoken_address, ) @@ -112,7 +112,7 @@ def test_pool_contract(self) -> None: # TODO: test syncing after time travel def test_sync(self) -> None: print("----==== TestAavePool | test_sync ====----") - pool = AaveV3DefaultInterestRatePool( + pool = AaveV3DefaultInterestRateV2Pool( contract_address=self.atoken_address, ) @@ -133,7 +133,7 @@ def test_sync(self) -> None: # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: print("----==== TestAavePool | test_supply_rate_increase_alloc ====----") - pool = AaveV3DefaultInterestRatePool( + pool = AaveV3DefaultInterestRateV2Pool( contract_address=self.atoken_address, ) @@ -153,7 +153,7 @@ def test_supply_rate_alloc(self) -> None: def test_supply_rate_decrease_alloc(self) -> None: print("----==== TestAavePool | test_supply_rate_decrease_alloc ====----") - pool = AaveV3DefaultInterestRatePool(contract_address=self.atoken_address, user_address=self.account.address) + pool = AaveV3DefaultInterestRateV2Pool(contract_address=self.atoken_address, user_address=self.account.address) # sync pool params pool.sync(web3_provider=self.w3) @@ -908,7 +908,7 @@ def tearDown(self) -> None: def test_pool_contract(self) -> None: print("----==== test_pool_contract ====----") # we call the aave3 weth atoken proxy contract in this example - pool = AaveV3DefaultInterestRatePool( + pool = AaveV3DefaultInterestRateV2Pool( contract_address=self.atoken_address, ) @@ -922,7 +922,7 @@ def test_pool_contract(self) -> None: # TODO: test syncing after time travel def test_sync(self) -> None: print("----==== test_sync ====----") - pool = AaveV3DefaultInterestRatePool( + pool = AaveV3DefaultInterestRateV2Pool( contract_address=self.atoken_address, ) @@ -943,7 +943,7 @@ def test_sync(self) -> None: # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: print("----==== test_supply_rate_increase_alloc ====----") - pool = AaveV3DefaultInterestRatePool( + pool = AaveV3DefaultInterestRateV2Pool( contract_address=self.atoken_address, user_address=self.account.address ) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index a328340..3a47b3a 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -286,14 +286,14 @@ def test_check_allocations_aave(self) -> None: assets_and_pools = { "total_assets": int(200e18), "pools": { - A: AaveV3DefaultInterestRatePool( + A: AaveV3DefaultInterestRateV2Pool( user_address=ADDRESS_ZERO, contract_address=A, ), }, } - pool_a: AaveV3DefaultInterestRatePool = assets_and_pools["pools"][A] + pool_a: AaveV3DefaultInterestRateV2Pool = assets_and_pools["pools"][A] pool_a.sync(self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available From 19f0ddd1dabcb096201c55ace13ff6e6b2b41978 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 12 Nov 2024 23:12:09 +0000 Subject: [PATCH 22/51] chore: use np.random.shuffle --- sturdy/validator/forward.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 8f2dd9d..a2c3c78 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -18,7 +18,6 @@ import asyncio import uuid -import random from typing import Any import bittensor as bt @@ -133,7 +132,7 @@ async def query_and_score_miners( # TODO: write custom availability function later down the road active_uids = [str(uid) for uid in range(self.metagraph.n.item()) if self.metagraph.axons[uid].is_serving] - random.shuffle(active_uids) + np.random.shuffle(active_uids) bt.logging.debug(f"active_uids: {active_uids}") From 9b0a24c2261e55363549a0d90e2336cd497c4b34 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Wed, 13 Nov 2024 09:41:40 +0000 Subject: [PATCH 23/51] feat: first set of new integration tests for validator --- .gitignore | 1 + neurons/validator.py | 8 +- sturdy/constants.py | 8 +- sturdy/pools.py | 4 +- sturdy/protocol.py | 1 - sturdy/utils/config.py | 11 +- sturdy/validator/forward.py | 32 +- sturdy/validator/reward.py | 14 +- sturdy/validator/sql.py | 11 +- .../validator/test_integration_validator.py | 323 +++++++++++------- 10 files changed, 241 insertions(+), 172 deletions(-) diff --git a/.gitignore b/.gitignore index 5267153..c381fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ pyrightconfig.json # databases db/schema.sql validator_database.db +*test.db # backups *.bak diff --git a/neurons/validator.py b/neurons/validator.py index eee2a58..22ecdb0 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -36,7 +36,7 @@ # import base validator class which takes care of most of the boilerplate from sturdy.base.validator import BaseValidatorNeuron -from sturdy.constants import ORGANIC_SCORING_PERIOD +from sturdy.constants import DB_DIR, ORGANIC_SCORING_PERIOD # Bittensor Validator Template: from sturdy.pools import PoolFactory @@ -267,8 +267,9 @@ async def get_allocations( miner_uid: str | None = None, from_ts: int | None = None, to_ts: int | None = None, + db_dir: str = DB_DIR, ) -> list[dict]: - with sql.get_db_connection() as conn: + with sql.get_db_connection(db_dir) as conn: allocations = sql.get_miner_responses(conn, request_uid, miner_uid, from_ts, to_ts) if not allocations: raise HTTPException(status_code=404, detail="No allocations found") @@ -280,8 +281,9 @@ async def request_info( request_uid: str | None = None, from_ts: int | None = None, to_ts: int | None = None, + db_dir: str = DB_DIR, ) -> list[dict]: - with sql.get_db_connection() as conn: + with sql.get_db_connection(db_dir) as conn: info = sql.get_request_info(conn, request_uid, from_ts, to_ts) if not info: raise HTTPException(status_code=404, detail="No request info found") diff --git a/sturdy/constants.py b/sturdy/constants.py index 2284ba6..1064e71 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -28,20 +28,20 @@ STOCHASTICITY_STEP = 0.0001 POOL_RESERVE_SIZE = int(100e18) # 100 -QUERY_RATE = 20 # how often synthetic validator queries miners (blocks) +QUERY_RATE = 20 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) ORGANIC_SCORING_PERIOD = 7200 # organic scoring period in seconds MIN_SCORING_PERIOD = 5400 # min. synthetic scoring period in seconds -MAX_SCORING_PERIOD = 10800 # max. synthetic scoring period in seconds +MAX_SCORING_PERIOD = 10800 # max. synthetic scoring period in seconds SCORING_PERIOD_STEP = 1800 -SCORING_WINDOW = 300 # scoring window +SCORING_WINDOW = 300 # scoring window TOTAL_ALLOC_THRESHOLD = 0.98 SIMILARITY_THRESHOLD = 0.01 # similarity threshold for plagiarism checking - +DB_DIR = "validator_database.db" # default validator database dir # The following constants are for different pool models # Aave diff --git a/sturdy/pools.py b/sturdy/pools.py index d00afd6..0b9d5c3 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -20,7 +20,7 @@ from decimal import Decimal from enum import IntEnum from pathlib import Path -from typing import Any, ClassVar, Literal +from typing import Any, ClassVar import bittensor as bt import numpy as np @@ -676,7 +676,7 @@ def __eq__(self, other) -> bool: other._pair_contract.address, ) - def pool_init(self, web3_provider: Web3) -> None: # noqa: ARG002 + def pool_init(self, web3_provider: Web3) -> None: try: assert web3_provider.is_connected() except Exception as err: diff --git a/sturdy/protocol.py b/sturdy/protocol.py index d985076..deeb6c7 100644 --- a/sturdy/protocol.py +++ b/sturdy/protocol.py @@ -17,7 +17,6 @@ # DEALINGS IN THE SOFTWARE. from enum import IntEnum -from typing import Annotated import bittensor as bt from pydantic import BaseModel, Field, root_validator, validator diff --git a/sturdy/utils/config.py b/sturdy/utils/config.py index 1c41cbc..8499ba3 100644 --- a/sturdy/utils/config.py +++ b/sturdy/utils/config.py @@ -24,7 +24,7 @@ from loguru import logger from sturdy import __spec_version__ as spec_version -from sturdy.constants import QUERY_TIMEOUT +from sturdy.constants import DB_DIR, QUERY_TIMEOUT def check_config(cls, config: "bt.Config") -> None: @@ -260,6 +260,15 @@ def add_validator_args(cls, parser): default=False, ) + # TODO: make this available for organic validators so that it can be used to in prod + # - not just testing? + parser.add_argument( + "--db_dir", + type=str, + help="directory of database - used for testing purposes", + default=DB_DIR, + ) + def config(cls) -> bt.config: """ diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index a2c3c78..b114a43 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -22,11 +22,11 @@ import bittensor as bt import numpy as np +from web3 import Web3 from web3.constants import ADDRESS_ZERO from sturdy.constants import MAX_SCORING_PERIOD, MIN_SCORING_PERIOD, QUERY_TIMEOUT, SCORING_PERIOD_STEP -from sturdy.pool_registry.pool_registry import POOL_REGISTRY -from sturdy.pools import POOL_TYPES, assets_pools_for_challenge_data, generate_challenge_data +from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations @@ -58,21 +58,12 @@ async def forward(self) -> Any: assets_and_pools = challenge_data["assets_and_pools"] pools = assets_and_pools["pools"] - metadata = {} + metadata = get_metadata(pools, self.w3) - for contract_addr, pool in pools.items(): - pool.sync(self.w3) - match pool.pool_type: - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): - metadata[contract_addr] = pool._share_price - case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - metadata[contract_addr] = pool._normalized_income - case _: - pass scoring_period = get_scoring_period() - with get_db_connection() as conn: + with get_db_connection(self.config.db_dir) as conn: log_allocations( conn, request_uuid, @@ -84,6 +75,19 @@ async def forward(self) -> Any: scoring_period, ) +def get_metadata(pools: dict[str, ChainBasedPoolModel], w3: Web3) -> dict: + metadata = {} + for contract_addr, pool in pools.items(): + pool.sync(w3) + match pool.pool_type: + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): + metadata[contract_addr] = pool._share_price + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): + metadata[contract_addr] = pool._normalized_income + case _: + pass + + return metadata def get_scoring_period(rng_gen: np.random.RandomState = None) -> int: if rng_gen is None: @@ -163,7 +167,7 @@ async def query_and_score_miners( # get all the request ids for the pools we should be scoring from the db active_alloc_rows = [] - with get_db_connection() as conn: + with get_db_connection(self.config.db_dir) as conn: active_alloc_rows = get_active_allocs(conn) bt.logging.debug(f"Active allocs: {active_alloc_rows}") diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 7ecdbb1..76a3244 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -24,12 +24,11 @@ import numpy as np import numpy.typing as npt import torch -from web3.constants import ADDRESS_ZERO from sturdy.constants import QUERY_TIMEOUT, SIMILARITY_THRESHOLD from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, PoolFactory, check_allocations from sturdy.protocol import AllocationsDict, AllocInfo -from sturdy.utils.ethmath import wei_div, wei_mul +from sturdy.utils.ethmath import wei_div from sturdy.validator.sql import get_db_connection, get_miner_responses, get_request_info @@ -85,7 +84,7 @@ def format_allocations( def normalize_squared( - apys_and_allocations: AllocationsDict, z_threshold: float = 1.0, q: float = 0.75, epsilon: float = 1e-8 + apys_and_allocations: AllocationsDict, epsilon: float = 1e-8 ) -> torch.Tensor: raw_apys = {uid: apys_and_allocations[uid]["apy"] for uid in apys_and_allocations} @@ -93,7 +92,7 @@ def normalize_squared( if len(raw_apys) <= 1: return torch.zeros(len(raw_apys)) - apys = torch.tensor(list(raw_apys.values())) + apys = torch.tensor(list(raw_apys.values())).to(torch.float64) squared = torch.pow(apys, 2) @@ -257,6 +256,7 @@ def _get_rewards( return adjust_rewards_for_plagiarism(self, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times) +# TODO: make this return annualized pct return instead of pct return within scoring period? def generated_yield_pct( allocations: AllocationsDict, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], extra_metadata: dict ) -> int: @@ -357,7 +357,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: assets_and_pools = None miners = None - with get_db_connection() as conn: + with get_db_connection(self.config.db_dir) as conn: # get assets and pools that are used to benchmark miner # we get the first row entry - we assume that it is the only response from the database try: @@ -389,9 +389,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: assets_and_pools["pools"] = new_pools - # TODO: this probably needs more work - # TODO: would it be better here to use metrics i.e. liquidityIndex for aave pools? - # calculate the "adjusted" yields of the allocations + # calculate the yield the pools accrued during the scoring period for miner in miners: allocations = json.loads(miner["allocation"])["allocations"] extra_metadata = json.loads(request_info["metadata"]) diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 2277961..9a43bed 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -7,7 +7,7 @@ from fastapi.encoders import jsonable_encoder -from sturdy.constants import SCORING_WINDOW +from sturdy.constants import DB_DIR, SCORING_WINDOW from sturdy.protocol import AllocInfo, ChainBasedPoolModel BALANCE = "balance" @@ -32,8 +32,8 @@ @contextmanager -def get_db_connection(): # noqa: ANN201 - conn = sqlite3.connect("validator_database.db") +def get_db_connection(db_dir: str = DB_DIR, uri: bool = False): # noqa: ANN201 + conn = sqlite3.connect(db_dir, uri=uri) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") try: @@ -197,10 +197,7 @@ def log_allocations( # TODO: rename function and database table? -def get_active_allocs( - conn: sqlite3.Connection, - scoring_window: float = SCORING_WINDOW -) -> list: +def get_active_allocs(conn: sqlite3.Connection, scoring_window: float = SCORING_WINDOW) -> list: # TODO: change the logic of handling "active allocations" # for now we simply get ones which are still in their "challenge" # period, and consider them to determine the score of miners diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index 48d8027..b722043 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -1,231 +1,290 @@ import os +import sqlite3 import unittest +import uuid from copy import copy +from pathlib import Path from unittest import IsolatedAsyncioTestCase +import bittensor as bt import numpy as np import torch from dotenv import load_dotenv +from freezegun import freeze_time from web3 import Web3 from neurons.validator import Validator from sturdy.algo import naive_algorithm -from sturdy.constants import QUERY_TIMEOUT from sturdy.mock import MockDendrite -from sturdy.pools import generate_challenge_data +from sturdy.pool_registry.pool_registry import POOL_REGISTRY +from sturdy.pools import assets_pools_for_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets -from sturdy.validator.forward import query_and_score_miners -from sturdy.validator.reward import get_rewards +from sturdy.validator.forward import get_metadata, query_multiple_miners +from sturdy.validator.reward import filter_allocations, get_rewards +from sturdy.validator.sql import get_active_allocs, get_db_connection, get_request_info, log_allocations load_dotenv() EXTERNAL_WEB3_PROVIDER_URL = os.getenv("WEB3_PROVIDER_URL") os.environ["WEB_PROVIDER_URL"] = "http://127.0.0.1:8545" +TEST_DB = "test.db" + + +def init_db(conn: sqlite3.Connection) -> None: + query = """CREATE TABLE IF NOT EXISTS allocation_requests ( + request_uid TEXT PRIMARY KEY, + assets_and_pools TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE active_allocs ( + request_uid TEXT PRIMARY KEY, + scoring_period_end TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) + ); + + CREATE TABLE IF NOT EXISTS allocations ( + request_uid TEXT, + miner_uid TEXT, + allocation TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (request_uid, miner_uid), + FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) + ); + + -- This alter statement adds a new column to the allocations table if it exists + ALTER TABLE allocation_requests + ADD COLUMN request_type TEXT NOT NULL DEFAULT 1; + ALTER TABLE allocation_requests + ADD COLUMN metadata TEXT; + ALTER TABLE allocations + ADD COLUMN axon_time FLOAT NOT NULL DEFAULT 99999.0; -- large number for now""" + + conn.executescript(query) + # TODO: more comprehensive integration testing - with in-mem sql db and everythin' class TestValidator(IsolatedAsyncioTestCase): - maxDiff = 4000 - @classmethod def setUpClass(cls) -> None: np.random.seed(69) # noqa: NPY002 - config = { + cls.config = { "mock": True, "wandb": {"off": True}, "mock_n": 16, "neuron": {"dont_save_events": True}, + "db_dir": TEST_DB, } - cls.validator = Validator(config=config) - w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) - cls.validator.w3 = w3 - assert cls.validator.w3.is_connected() + cls.w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) + assert cls.w3.is_connected() - cls.validator.w3.provider.make_request( + cls.w3.provider.make_request( "hardhat_reset", # type: ignore[] [ { "forking": { "jsonRpcUrl": EXTERNAL_WEB3_PROVIDER_URL, - "blockNumber": 21080765, + "blockNumber": 21147890, }, }, ], ) - generated_data = generate_challenge_data(cls.validator.w3, np.random.RandomState(seed=420)) - cls.assets_and_pools = generated_data["assets_and_pools"] + selected_entry = POOL_REGISTRY["Sturdy Crvusd Aggregator"] + cls.generated_data = assets_pools_for_challenge_data(selected_entry, cls.w3) + print(f"assets and pools: {cls.generated_data}") + cls.assets_and_pools = cls.generated_data["assets_and_pools"] synapse = AllocateAssets( request_type=REQUEST_TYPES.SYNTHETIC, assets_and_pools=copy(cls.assets_and_pools), ) - cls.allocations = naive_algorithm(cls.validator, synapse) + cls.allocations = naive_algorithm(cls, synapse) + cls.user_address = cls.generated_data["user_address"] cls.contract_addresses: list[str] = list(cls.assets_and_pools["pools"].keys()) # type: ignore[] - async def test_get_rewards(self) -> None: - print("----==== test_get_rewards ====----") + @classmethod + def tearDownClass(cls) -> None: + # run this after tests to restore original forked state + w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) - assets_and_pools = copy(self.assets_and_pools) - allocations = copy(self.allocations) + w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": EXTERNAL_WEB3_PROVIDER_URL, + "blockNumber": 21150770, + }, + }, + ], + ) - validator = self.validator + def setUp(self) -> None: + # purge sql db + path = Path(TEST_DB) + if path.exists(): + path.unlink() - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] + self.snapshot_id = self.w3.provider.make_request("evm_snapshot", []) # type: ignore[] + print(f"snapshot id: {self.snapshot_id}") - active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] + self.validator = Validator(config=self.config) + self.validator.w3 = self.w3 + assert self.validator.w3.is_connected() - synapse = AllocateAssets( - request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy(assets_and_pools), - allocations=copy(allocations), - ) + # init sql db + with get_db_connection(TEST_DB, True) as conn: + init_db(conn) + cur = conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [dict(t) for t in cur.fetchall()] + print(f"tables init: {tables}") - validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) - responses = await validator.dendrite( - # Send the query to selected miner axons in the network. - axons=active_axons, - # Construct a dummy query. This simply contains a single integer. - synapse=synapse, - deserialize=False, - timeout=QUERY_TIMEOUT, - ) + def tearDown(self) -> None: + # Optional: Revert to the original snapshot after each test + print("reverting to original evm snapshot") + self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] - for response in responses: - # TODO: is this necessary? - # self.assertEqual(response.assets_and_pools, self.assets_and_pools) - self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) + # purge sql db + path = Path(TEST_DB) + if path.exists(): + path.unlink() - rewards, allocs = get_rewards( - validator, - validator.step, - active_uids, - responses=responses, - assets_and_pools=assets_and_pools, - ) + async def test_get_rewards(self) -> None: + print("----==== test_get_rewards ====----") - print(f"allocs: {allocs}") + freezer = freeze_time("2024-01-11 00:00:00") + freezer.start() - rewards_dict = {active_uids[k]: v for k, v in enumerate(list(rewards))} - sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + request_uuid = str(uuid.uuid4()).replace("-", "") - print(f"sorted rewards: {sorted_rewards}") + with get_db_connection(self.validator.config.db_dir, True) as conn: + cur = conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [dict(t) for t in cur.fetchall()] + print(f"tables: {tables}") - # rewards should not all be the same - to_compare = torch.empty(rewards.shape) - torch.fill(to_compare, rewards[0]) - self.assertFalse(torch.equal(rewards, to_compare)) + assets_and_pools = copy(self.assets_and_pools) + allocations = copy(self.allocations) - async def test_get_rewards_punish(self) -> None: - print("----==== test_get_rewards_punish ====----") validator = self.validator - assets_and_pools = copy(self.assets_and_pools) + validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) - allocations = copy(self.allocations) - # increase one of the allocations by +10000 -> clearly this means the miner is cheating!!! - allocations[self.contract_addresses[0]] += int(10000e18) + # ==== + + active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] + np.random.shuffle(active_uids) - active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] + print(f"active_uids: {active_uids}") synapse = AllocateAssets( request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy(assets_and_pools), - allocations=copy(allocations), + assets_and_pools=assets_and_pools, + user_address=self.user_address, ) - validator.dendrite = MockDendrite(wallet=validator.wallet) - responses = await validator.dendrite( - # Send the query to selected miner axons in the network. - axons=active_axons, - # Construct a dummy query. This simply contains a single integer. - synapse=synapse, - deserialize=False, - timeout=QUERY_TIMEOUT, + # query all miners + responses = await query_multiple_miners( + validator, + synapse, + active_uids, ) + allocations = {uid: responses[idx].allocations for idx, uid in enumerate(active_uids)} # type: ignore[] + for response in responses: # TODO: is this necessary? - # self.assertEqual(response.assets_and_pools, assets_and_pools) - self.assertEqual(response.allocations, allocations) + # self.assertEqual(response.assets_and_pools, self.assets_and_pools) + self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) - rewards, allocs = get_rewards( - validator, - validator.step, - active_uids, + # Log the results for monitoring purposes. + print(f"Assets and pools: {synapse.assets_and_pools}") + print(f"Received allocations (uid -> allocations): {allocations}") + + pools = assets_and_pools["pools"] + metadata = get_metadata(pools, validator.w3) + + # axon_times = get_response_times(uids=active_uids, responses=responses, timeout=QUERY_TIMEOUT) + # scoring period is ~12 hours + scoring_period = 43200 + + axon_times, filtered_allocs = filter_allocations( + self, + query=validator.step, + uids=active_uids, responses=responses, assets_and_pools=assets_and_pools, ) - for allocInfo in allocs.values(): - self.assertEqual(allocInfo["apy"], 0) - - # rewards should all be the same (0) - self.assertEqual(all(rewards), 0) + # log allocations + with get_db_connection(validator.config.db_dir) as conn: + log_allocations( + conn, + request_uuid, + assets_and_pools, + metadata, + filtered_allocs, + axon_times, + REQUEST_TYPES.SYNTHETIC, + scoring_period, + ) - rewards_dict = dict(enumerate(list(rewards))) - sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + freezer.stop() - print(f"sorted rewards: {sorted_rewards}") + # fast forward ~12 hrs - assets_and_pools = copy(self.assets_and_pools) - - allocations = copy(self.allocations) - # set one of the allocations to be negative! This should not be allowed! - allocations[self.contract_addresses[0]] = -1 + freezer = freeze_time("2024-01-11 12:01:00") + freezer.start() - active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] # type: ignore[] - - active_axons = [validator.metagraph.axons[int(uid)] for uid in active_uids] - - synapse = AllocateAssets( - request_type=REQUEST_TYPES.SYNTHETIC, - assets_and_pools=copy(assets_and_pools), - allocations=copy(allocations), + validator.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": EXTERNAL_WEB3_PROVIDER_URL, + "blockNumber": 21150770, + }, + }, + ], ) - validator.dendrite = MockDendrite(wallet=validator.wallet) - responses = await validator.dendrite( - # Send the query to selected miner axons in the network. - axons=active_axons, - # Construct a dummy query. This simply contains a single integer. - synapse=synapse, - deserialize=False, - timeout=QUERY_TIMEOUT, - ) + curr_pools = assets_and_pools["pools"] + for pool in curr_pools.values(): + pool.sync(validator.w3) - for response in responses: - # TODO: is this necessary? - # self.assertEqual(response.assets_and_pools, assets_and_pools) - self.assertEqual(response.allocations, allocations) + # score previously suggested miner allocations based on how well they are performing now + # get all the request ids for the pools we should be scoring from the db + active_alloc_rows = [] + with get_db_connection(validator.config.db_dir, True) as conn: + active_alloc_rows = get_active_allocs(conn) - rewards, allocs = get_rewards( - validator, - validator.step, - active_uids, - responses=responses, - assets_and_pools=assets_and_pools, - ) + print(f"Active allocs: {active_alloc_rows}") - for allocInfo in allocs.values(): - self.assertEqual(allocInfo["apy"], 0) + with get_db_connection(validator.config.db_dir, True) as conn: + all_requests = get_request_info(conn) + print(f"all requests: {all_requests}") - # rewards should all be the same (0) - self.assertEqual(all(rewards), 0) + for active_alloc in active_alloc_rows: + # calculate rewards for previous active allocations + miner_uids, rewards = get_rewards(validator, active_alloc) - rewards_dict = dict(enumerate(list(rewards))) - sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + rewards_dict = {active_uids[k]: v for k, v in enumerate(list(rewards))} + sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] - print(f"sorted rewards: {sorted_rewards}") + print(f"sorted rewards: {sorted_rewards}") + # bt.logging.debug(f"miner rewards: {rewards}") + print(f"sim penalities: {validator.similarity_penalties}") - async def test_query_and_score_miners(self) -> None: - await query_and_score_miners(self.validator, assets_and_pools=self.assets_and_pools) + # rewards should not all be the same + to_compare = torch.empty(rewards.shape) + torch.fill(to_compare, rewards[0]) + self.assertFalse(torch.equal(rewards, to_compare)) - async def test_forward(self) -> None: - await self.validator.forward() + freezer.stop() if __name__ == "__main__": From 53d5bbf402168bac283d1112dc637e152a2cb4de Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Thu, 14 Nov 2024 09:18:47 +0000 Subject: [PATCH 24/51] feat: update yield calc + renaming stuff --- README.md | 2 +- sturdy/pools.py | 86 +++++++++---------- sturdy/utils/misc.py | 11 +++ sturdy/validator/reward.py | 34 +++++--- .../validator/test_integration_validator.py | 4 +- tests/unit/validator/test_pool_models.py | 8 +- tests/unit/validator/test_reward_helpers.py | 60 ++++++------- 7 files changed, 114 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 1f27018..97614f1 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ There are three core files. total_asset = 0 match pool.pool_type: case POOL_TYPES.STURDY_SILO: - total_asset += pool._curr_deposit_amount + total_asset += pool._user_deposits case _: pass diff --git a/sturdy/pools.py b/sturdy/pools.py index 0b9d5c3..ec74a8c 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -59,25 +59,25 @@ def get_minimum_allocation(pool: "ChainBasedPoolModel") -> int: match pool.pool_type: case POOL_TYPES.STURDY_SILO: borrow_amount = pool._totalBorrow - our_supply = pool._curr_deposit_amount - assets_available = max(0, pool._totalAssets - borrow_amount) + our_supply = pool._user_deposits + assets_available = max(0, pool._total_supplied_assets - borrow_amount) case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): # borrow amount for aave pools is total_stable_debt + total_variable_debt borrow_amount = ((pool._nextTotalStableDebt * int(1e18)) // int(10**pool._decimals)) + ( (pool._totalVariableDebt * int(1e18)) // int(10**pool._decimals) ) - our_supply = pool._collateral_amount - assets_available = max(0, ((pool._total_supplied * int(1e18)) // int(10**pool._decimals)) - borrow_amount) + our_supply = pool._user_deposits + assets_available = max(0, ((pool._total_supplied_assets * int(1e18)) // int(10**pool._decimals)) - borrow_amount) case POOL_TYPES.COMPOUND_V3: borrow_amount = pool._total_borrow - our_supply = pool._deposit_amount - assets_available = max(0, pool._total_supply - borrow_amount) + our_supply = pool._user_deposits + assets_available = max(0, pool._total_supplied_assets - borrow_amount) case POOL_TYPES.MORPHO: borrow_amount = pool._curr_borrows - our_supply = pool._user_assets - assets_available = max(0, pool._total_assets - borrow_amount) + our_supply = pool._user_deposits + assets_available = max(0, pool._total_supplied_assets - borrow_amount) case POOL_TYPES.YEARN_V3: - return max(0, pool._curr_deposit - pool._max_withdraw) + return max(0, pool._user_deposits - pool._max_withdraw) case POOL_TYPES.DAI_SAVINGS: pass # TODO: is there a more appropriate way to go about this? case _: # not a valid pool type @@ -234,8 +234,8 @@ class AaveV3DefaultInterestRateV2Pool(ChainBasedPoolModel): _variable_debt_token_contract = PrivateAttr() _totalVariableDebt = PrivateAttr() _reserveFactor = PrivateAttr() - _collateral_amount: int = PrivateAttr() - _total_supplied: int = PrivateAttr() + _user_deposits: int = PrivateAttr() + _total_supplied_assets: int = PrivateAttr() _decimals: int = PrivateAttr() _user_asset_balance: int = PrivateAttr() _normalized_income: int = PrivateAttr() @@ -299,7 +299,7 @@ def pool_init(self, web3_provider: Web3) -> None: address=self._underlying_asset_address, ) - self._total_supplied = retry_with_backoff(self._atoken_contract.functions.totalSupply().call) + self._total_supplied_assets = retry_with_backoff(self._atoken_contract.functions.totalSupply().call) self._initted = True @@ -379,7 +379,7 @@ def sync(self, web3_provider: Web3) -> None: reserveConfiguration = self._reserve_data.configuration self._reserveFactor = getReserveFactor(reserveConfiguration) self._decimals = retry_with_backoff(self._underlying_asset_contract.functions.decimals().call) - self._collateral_amount = retry_with_backoff( + self._user_deposits = retry_with_backoff( self._atoken_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call ) @@ -400,7 +400,7 @@ def sync(self, web3_provider: Web3) -> None: def supply_rate(self, amount: int) -> int: """Returns supply rate given new deposit amount""" try: - already_deposited = self._collateral_amount + already_deposited = self._user_deposits delta = amount - already_deposited to_deposit = max(0, delta) to_remove = abs(delta) if delta < 0 else 0 @@ -445,8 +445,8 @@ class AaveV3RateTargetBaseInterestRatePool(ChainBasedPoolModel): _variable_debt_token_contract = PrivateAttr() _totalVariableDebt = PrivateAttr() _reserveFactor = PrivateAttr() - _collateral_amount: int = PrivateAttr() - _total_supplied: int = PrivateAttr() + _user_deposits: int = PrivateAttr() + _total_supplied_assets: int = PrivateAttr() _decimals: int = PrivateAttr() _user_asset_balance: int = PrivateAttr() _normalized_income: int = PrivateAttr() @@ -510,7 +510,7 @@ def pool_init(self, web3_provider: Web3) -> None: address=self._underlying_asset_address, ) - self._total_supplied = retry_with_backoff(self._atoken_contract.functions.totalSupply().call) + self._total_supplied_assets = retry_with_backoff(self._atoken_contract.functions.totalSupply().call) self._initted = True @@ -590,7 +590,7 @@ def sync(self, web3_provider: Web3) -> None: reserveConfiguration = self._reserve_data.configuration self._reserveFactor = getReserveFactor(reserveConfiguration) self._decimals = retry_with_backoff(self._underlying_asset_contract.functions.decimals().call) - self._collateral_amount = retry_with_backoff( + self._user_deposits = retry_with_backoff( self._atoken_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call ) @@ -611,7 +611,7 @@ def sync(self, web3_provider: Web3) -> None: def supply_rate(self, amount: int) -> int: """Returns supply rate given new deposit amount""" try: - already_deposited = self._collateral_amount + already_deposited = self._user_deposits delta = amount - already_deposited to_deposit = max(0, delta) to_remove = abs(delta) if delta < 0 else 0 @@ -648,10 +648,10 @@ class VariableInterestSturdySiloStrategy(ChainBasedPoolModel): _pair_contract: Contract = PrivateAttr() _rate_model_contract: Contract = PrivateAttr() - _curr_deposit_amount: int = PrivateAttr() + _user_deposits: int = PrivateAttr() _util_prec: int = PrivateAttr() _fee_prec: int = PrivateAttr() - _totalAssets: Any = PrivateAttr() + _total_supplied_assets: Any = PrivateAttr() _totalBorrow: Any = PrivateAttr() _current_rate_info = PrivateAttr() _rate_prec: int = PrivateAttr() @@ -731,12 +731,12 @@ def sync(self, web3_provider: Web3) -> None: self.pool_init(web3_provider) user_shares = retry_with_backoff(self._pair_contract.functions.balanceOf(self.contract_address).call) - self._curr_deposit_amount = retry_with_backoff(self._pair_contract.functions.convertToAssets(user_shares).call) + self._user_deposits = retry_with_backoff(self._pair_contract.functions.convertToAssets(user_shares).call) constants = retry_with_backoff(self._pair_contract.functions.getConstants().call) self._util_prec = constants[2] self._fee_prec = constants[3] - self._totalAssets: Any = retry_with_backoff(self._pair_contract.functions.totalAssets().call) + self._total_supplied_assets: Any = retry_with_backoff(self._pair_contract.functions.totalAssets().call) self._totalBorrow: Any = retry_with_backoff(self._pair_contract.functions.totalBorrow().call).amount self._block = web3_provider.eth.get_block("latest") @@ -754,10 +754,10 @@ def sync(self, web3_provider: Web3) -> None: @ttl_cache(maxsize=256, ttl=60) def supply_rate(self, amount: int) -> int: # amount scaled down to the asset's decimals from 18 decimals (wei) - delta = amount - self._curr_deposit_amount + delta = amount - self._user_deposits """Returns supply rate given new deposit amount""" - util_rate = int((self._util_prec * self._totalBorrow) // (self._totalAssets + delta)) + util_rate = int((self._util_prec * self._totalBorrow) // (self._total_supplied_assets + delta)) last_update_timestamp = self._current_rate_info.lastTimestamp current_timestamp = self._block["timestamp"] @@ -797,8 +797,8 @@ class CompoundV3Pool(ChainBasedPoolModel): _reward_token_price: float = PrivateAttr() _base_decimals: int = PrivateAttr() _total_borrow: int = PrivateAttr() - _deposit_amount: int = PrivateAttr() - _total_supply: int = PrivateAttr() + _user_deposits: int = PrivateAttr() + _total_supplied_assets: int = PrivateAttr() _CompoundTokenMap: dict = { "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # WETH -> ETH @@ -862,16 +862,16 @@ def sync(self, web3_provider: Web3) -> None: retry_with_backoff(self._reward_oracle_contract.functions.latestAnswer().call) / 10**reward_decimals ) - self._deposit_amount = retry_with_backoff(self._ctoken_contract.functions.balanceOf(self.user_address).call) - self._total_supply = retry_with_backoff(self._ctoken_contract.functions.totalSupply().call) + self._user_deposits = retry_with_backoff(self._ctoken_contract.functions.balanceOf(self.user_address).call) + self._total_supplied_assets = retry_with_backoff(self._ctoken_contract.functions.totalSupply().call) def supply_rate(self, amount: int) -> int: # amount scaled down to the asset's decimals from 18 decimals (wei) # get pool supply rate (base token) - already_in_pool = self._deposit_amount + already_in_pool = self._user_deposits delta = amount - already_in_pool - new_supply = self._total_supply + delta + new_supply = self._total_supplied_assets + delta current_borrows = self._total_borrow utilization = wei_div(current_borrows, new_supply) @@ -961,8 +961,8 @@ class MorphoVault(ChainBasedPoolModel): _DECIMALS_OFFSET: int = PrivateAttr() # TODO: update unit tests to check these :^) _irm_contracts: dict = PrivateAttr(default={}) - _total_assets: int = PrivateAttr() - _user_assets: int = PrivateAttr() + _total_supplied_assets: int = PrivateAttr() + _user_deposits: int = PrivateAttr() _curr_borrows: int = PrivateAttr() _asset_decimals: int = PrivateAttr() _underlying_asset_contract: Contract = PrivateAttr() @@ -1047,9 +1047,9 @@ def sync(self, web3_provider: Web3) -> None: total_borrows += market.totalBorrowAssets - self._total_assets = retry_with_backoff(self._vault_contract.functions.totalAssets().call) + self._total_supplied_assets = retry_with_backoff(self._vault_contract.functions.totalAssets().call) curr_user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) - self._user_assets = retry_with_backoff(self._vault_contract.functions.convertToAssets(curr_user_shares).call) + self._user_deposits = retry_with_backoff(self._vault_contract.functions.convertToAssets(curr_user_shares).call) self._user_asset_balance = retry_with_backoff( self._underlying_asset_contract.functions.balanceOf(Web3.to_checksum_address(self.user_address)).call ) @@ -1073,7 +1073,7 @@ def supply_rate(self, amount: int) -> int: retry_with_backoff(self._vault_contract.functions.supplyQueue(idx).call) for idx in range(supply_queue_length) ] - total_asset_delta = amount - self._user_assets + total_asset_delta = amount - self._user_deposits # apys in each market current_supply_apys = [] @@ -1115,7 +1115,7 @@ def supply_rate(self, amount: int) -> int: ) / sum(current_assets) return int( - (wei_mul(curr_agg_apy, self._total_assets) / (self._total_assets + total_asset_delta)) + (wei_mul(curr_agg_apy, self._total_supplied_assets) / (self._total_supplied_assets + total_asset_delta)) * 10 ** (self._asset_decimals * 2) ) @@ -1126,7 +1126,7 @@ class YearnV3Vault(ChainBasedPoolModel): _vault_contract: Contract = PrivateAttr() _apr_oracle: Contract = PrivateAttr() _max_withdraw: int = PrivateAttr() - _curr_deposit: int = PrivateAttr() + _user_deposits: int = PrivateAttr() def pool_init(self, web3_provider: Web3) -> None: vault_abi_file_path = Path(__file__).parent / "abi/Yearn_V3_Vault.json" @@ -1151,10 +1151,10 @@ def sync(self, web3_provider: Web3) -> None: self._max_withdraw = retry_with_backoff(self._vault_contract.functions.maxWithdraw(self.user_address).call) user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) - self._curr_deposit = retry_with_backoff(self._vault_contract.functions.convertToAssets(user_shares).call) + self._user_deposits = retry_with_backoff(self._vault_contract.functions.convertToAssets(user_shares).call) def supply_rate(self, amount: int) -> int: - delta = amount - self._curr_deposit + delta = amount - self._user_deposits return retry_with_backoff(self._apr_oracle.functions.getExpectedApr(self.contract_address, delta).call) @@ -1215,11 +1215,11 @@ def assets_pools_for_challenge_data( total_asset = 0 match pool.pool_type: case POOL_TYPES.STURDY_SILO: - total_asset += pool._curr_deposit_amount + total_asset += pool._user_deposits case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - total_asset += pool._collateral_amount + total_asset += pool._user_deposits case POOL_TYPES.MORPHO: - total_asset += pool._user_assets + total_asset += pool._user_deposits case _: pass diff --git a/sturdy/utils/misc.py b/sturdy/utils/misc.py index fb4a03e..2f81ee3 100644 --- a/sturdy/utils/misc.py +++ b/sturdy/utils/misc.py @@ -18,6 +18,7 @@ import time from collections.abc import Callable +from datetime import datetime, timezone from functools import lru_cache, update_wrapper from math import floor from typing import Any @@ -36,6 +37,16 @@ # TODO: cleanup functions - lay them out better across files? +def time_diff_seconds(start: str, end: str, format_str: str = "%Y-%m-%d %H:%M:%S.%f") -> int: + start_datetime = datetime.strptime(start, format_str).replace(tzinfo=timezone.utc) # noqa: UP017 + end_datetime = datetime.strptime(end, format_str).replace(tzinfo=timezone.utc) # noqa: UP017 + return (end_datetime - start_datetime).seconds + +def get_scoring_period_length(active_allocation: dict) -> int: + scoring_period_start = active_allocation["created_at"] + scoring_period_end = active_allocation["scoring_period_end"] + return time_diff_seconds(scoring_period_start, scoring_period_end) + # rand range but float def randrange_float( start, diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 76a3244..0808f62 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -29,6 +29,7 @@ from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, PoolFactory, check_allocations from sturdy.protocol import AllocationsDict, AllocInfo from sturdy.utils.ethmath import wei_div +from sturdy.utils.misc import get_scoring_period_length from sturdy.validator.sql import get_db_connection, get_miner_responses, get_request_info @@ -83,16 +84,14 @@ def format_allocations( return {contract_addr: allocs[contract_addr] for contract_addr in sorted(allocs.keys())} -def normalize_squared( - apys_and_allocations: AllocationsDict, epsilon: float = 1e-8 -) -> torch.Tensor: +def normalize_squared(apys_and_allocations: AllocationsDict, epsilon: float = 1e-8) -> torch.Tensor: raw_apys = {uid: apys_and_allocations[uid]["apy"] for uid in apys_and_allocations} # TODO: is there a better way to go about this? if len(raw_apys) <= 1: return torch.zeros(len(raw_apys)) - apys = torch.tensor(list(raw_apys.values())).to(torch.float64) + apys = torch.tensor(list(raw_apys.values())).to(torch.float32) squared = torch.pow(apys, 2) @@ -256,12 +255,14 @@ def _get_rewards( return adjust_rewards_for_plagiarism(self, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times) -# TODO: make this return annualized pct return instead of pct return within scoring period? -def generated_yield_pct( - allocations: AllocationsDict, assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], extra_metadata: dict +def annualized_yield_pct( + allocations: AllocationsDict, + assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], + seconds_passed: int, + extra_metadata: dict, ) -> int: """ - Calculates yields generated allocations in pools within scoring period + Calculates annualized yields of allocations in pools within scoring period """ # calculate projected yield @@ -269,6 +270,9 @@ def generated_yield_pct( pools = cast(dict[str, ChainBasedPoolModel], assets_and_pools["pools"]) total_yield = 0 + seconds_per_year = 31536000 + + # TODO: refactor? for contract_addr, pool in pools.items(): allocation = allocations[contract_addr] match pool.pool_type: @@ -276,12 +280,18 @@ def generated_yield_pct( last_share_price = extra_metadata[contract_addr] curr_share_price = pool._share_price pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) - total_yield += int(allocation * pct_delta) + deposit_delta = allocation - pool._user_deposits + adjusted_pct_delta = (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta) * pct_delta + annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 + total_yield += int(allocation * annualized_pct_yield) case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): last_income = extra_metadata[contract_addr] curr_income = pool._normalized_income pct_delta = float(curr_income - last_income) / float(last_income) - total_yield += int(allocation * pct_delta) + deposit_delta = allocation - pool._user_deposits + adjusted_pct_delta = (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta) * pct_delta + annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 + total_yield += int(allocation * annualized_pct_yield) case _: total_yield += 0 @@ -353,6 +363,8 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: # TODO: rename this here and in the database schema? request_uid = active_allocation["request_uid"] + scoring_period_length = get_scoring_period_length(active_allocation) + request_info = {} assets_and_pools = None miners = None @@ -394,7 +406,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: allocations = json.loads(miner["allocation"])["allocations"] extra_metadata = json.loads(request_info["metadata"]) miner_uid = miner["miner_uid"] - miner_apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) + miner_apy = annualized_yield_pct(allocations, assets_and_pools, scoring_period_length, extra_metadata) miner_axon_time = miner["axon_time"] miner_uids.append(miner_uid) diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index b722043..b3f3710 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -157,7 +157,7 @@ def tearDown(self) -> None: async def test_get_rewards(self) -> None: print("----==== test_get_rewards ====----") - freezer = freeze_time("2024-01-11 00:00:00") + freezer = freeze_time("2024-01-11 00:00:00.124513") freezer.start() request_uuid = str(uuid.uuid4()).replace("-", "") @@ -237,7 +237,7 @@ async def test_get_rewards(self) -> None: # fast forward ~12 hrs - freezer = freeze_time("2024-01-11 12:01:00") + freezer = freeze_time("2024-01-11 12:01:00.136136") freezer.start() validator.w3.provider.make_request( diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 8f74ed6..2afcdca 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -628,10 +628,10 @@ def test_morphovault_pool_model(self) -> None: self.assertTrue(hasattr(pool, "_asset_decimals")) self.assertTrue(isinstance(pool._asset_decimals, int)) - self.assertTrue(hasattr(pool, "_total_assets")) - self.assertTrue(isinstance(pool._total_assets, int)) - self.assertTrue(hasattr(pool, "_user_assets")) - self.assertTrue(isinstance(pool._user_assets, int)) + self.assertTrue(hasattr(pool, "_total_supplied_assets")) + self.assertTrue(isinstance(pool._total_supplied_assets, int)) + self.assertTrue(hasattr(pool, "_user_deposits")) + self.assertTrue(isinstance(pool._user_deposits, int)) self.assertTrue(hasattr(pool, "_curr_borrows")) self.assertTrue(isinstance(pool._curr_borrows, int)) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 3a47b3a..8ed7e81 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -15,10 +15,10 @@ from sturdy.protocol import REQUEST_TYPES, AllocateAssets from sturdy.validator.reward import ( adjust_rewards_for_plagiarism, + annualized_yield_pct, calculate_penalties, calculate_rewards_with_adjusted_penalties, format_allocations, - generated_yield_pct, get_distance, get_similarity_matrix, normalize_squared, @@ -234,9 +234,9 @@ def test_check_allocations_sturdy(self) -> None: pool_a.sync(web3_provider=self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available - pool_a._totalAssets = int(100e23) + pool_a._total_supplied_assets = int(100e23) pool_a._totalBorrow = int(10e23) - pool_a._curr_deposit_amount = int(5e23) + pool_a._user_deposits = int(5e23) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -244,7 +244,7 @@ def test_check_allocations_sturdy(self) -> None: # case: borrow_amount > assets_available, deposit_amount >= assets_available pool_a._totalBorrow = int(97e23) - pool_a._curr_deposit_amount = int(5e23) + pool_a._user_deposits = int(5e23) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations) @@ -252,7 +252,7 @@ def test_check_allocations_sturdy(self) -> None: # should return True pool_a._totalBorrow = int(97e23) - pool_a._curr_deposit_amount = int(5e23) + pool_a._user_deposits = int(5e23) allocations[A] = int(4e23) result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -260,7 +260,7 @@ def test_check_allocations_sturdy(self) -> None: # case: borrow_amount > assets_available, deposit_amount < assets_available pool_a._totalBorrow = int(10e23) - pool_a._curr_deposit_amount = int(1e23) + pool_a._user_deposits = int(1e23) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -297,10 +297,10 @@ def test_check_allocations_aave(self) -> None: pool_a.sync(self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available - pool_a._total_supplied = int(100e6) + pool_a._total_supplied_assets = int(100e6) pool_a._nextTotalStableDebt = 0 pool_a._totalVariableDebt = int(10e6) - pool_a._collateral_amount = int(5e18) + pool_a._user_deposits = int(5e18) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -309,7 +309,7 @@ def test_check_allocations_aave(self) -> None: # case: borrow_amount > assets_available, deposit_amount >= assets_available pool_a._nextTotalStableDebt = 0 pool_a._totalVariableDebt = int(97e6) - pool_a._collateral_amount = int(5e18) + pool_a._user_deposits = int(5e18) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -318,7 +318,7 @@ def test_check_allocations_aave(self) -> None: # should return True pool_a._nextTotalStableDebt = 0 pool_a._totalVariableDebt = int(97e6) - pool_a._collateral_amount = int(5e18) + pool_a._user_deposits = int(5e18) allocations[A] = int(4e18) result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -327,7 +327,7 @@ def test_check_allocations_aave(self) -> None: # case: borrow_amount > assets_available, deposit_amount < assets_available pool_a._nextTotalStableDebt = 0 pool_a._totalVariableDebt = int(97e6) - pool_a._collateral_amount = int(1e18) + pool_a._user_deposits = int(1e18) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -351,9 +351,9 @@ def test_check_allocations_compound(self) -> None: pool_a.sync(self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available - pool_a._total_supply = int(100e14) + pool_a._total_supplied_assets = int(100e14) pool_a._total_borrow = int(10e14) - pool_a._deposit_amount = int(5e14) + pool_a._user_deposits = int(5e14) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -361,7 +361,7 @@ def test_check_allocations_compound(self) -> None: # case: borrow_amount > assets_available, deposit_amount >= assets_available pool_a._total_borrow = int(97e14) - pool_a._deposit_amount = int(5e14) + pool_a._user_deposits = int(5e14) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -369,7 +369,7 @@ def test_check_allocations_compound(self) -> None: # should return True pool_a._total_borrow = int(97e14) - pool_a._deposit_amount = int(5e14) + pool_a._user_deposits = int(5e14) allocations[A] = int(4e26) result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -377,7 +377,7 @@ def test_check_allocations_compound(self) -> None: # case: borrow_amount > assets_available, deposit_amount < assets_available pool_a._total_borrow = int(97e14) - pool_a._deposit_amount = int(1e14) + pool_a._user_deposits = int(1e14) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -413,9 +413,9 @@ def test_check_allocations_morpho(self) -> None: pool_a.sync(self.w3) # case: borrow_amount <= assets_available, deposit_amount < assets_available - pool_a._total_assets = int(100e14) + pool_a._total_supplied_assets = int(100e14) pool_a._curr_borrows = int(10e14) - pool_a._user_assets = int(5e14) + pool_a._user_deposits = int(5e14) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -423,7 +423,7 @@ def test_check_allocations_morpho(self) -> None: # case: borrow_amount > assets_available, deposit_amount >= assets_available pool_a._curr_borrows = int(97e14) - pool_a._user_assets = int(5e14) + pool_a._user_deposits = int(5e14) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -431,7 +431,7 @@ def test_check_allocations_morpho(self) -> None: # should return True pool_a._curr_borrows = int(97e14) - pool_a._user_assets = int(5e14) + pool_a._user_deposits = int(5e14) allocations[A] = int(4e14) result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -439,7 +439,7 @@ def test_check_allocations_morpho(self) -> None: # case: borrow_amount > assets_available, deposit_amount < assets_available pool_a._curr_borrows = int(97e14) - pool_a._user_assets = int(1e14) + pool_a._user_deposits = int(1e14) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -464,7 +464,7 @@ def test_check_allocations_yearn(self) -> None: # case: max withdraw = deposit amount pool_a._max_withdraw = int(1e9) - pool_a._curr_deposit = int(1e9) + pool_a._user_deposits = int(1e9) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -472,7 +472,7 @@ def test_check_allocations_yearn(self) -> None: # case: max withdraw = 0 pool_a._max_withdraw = 0 - pool_a._curr_deposit = int(1e9) + pool_a._user_deposits = int(1e9) allocations[A] = 1 result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -480,7 +480,7 @@ def test_check_allocations_yearn(self) -> None: # should return True pool_a._max_withdraw = int(1e9) - pool_a._curr_deposit = int(5e9) + pool_a._user_deposits = int(5e9) allocations[A] = int(4e9) result = check_allocations(assets_and_pools, allocations, alloc_threshold=0) @@ -799,8 +799,8 @@ def test_calculate_apy_sturdy(self) -> None: for pool in assets_and_pools["pools"].values(): pool.sync(self.w3) - apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) - print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") + apy = annualized_yield_pct(allocations, assets_and_pools, 604800, extra_metadata) + print(f"annualized yield: {(float(apy)/1e18) * 100}%") self.assertGreater(apy, 0) def test_calculate_apy_aave(self) -> None: @@ -868,8 +868,8 @@ def test_calculate_apy_aave(self) -> None: for pool in assets_and_pools["pools"].values(): pool.sync(self.w3) - apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) - print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") + apy = annualized_yield_pct(allocations, assets_and_pools, 604800, extra_metadata) + print(f"annualized yield: {(float(apy)/1e18) * 100}%") self.assertGreater(apy, 0) def test_calculate_apy_morpho(self) -> None: @@ -918,8 +918,8 @@ def test_calculate_apy_morpho(self) -> None: for pool in assets_and_pools["pools"].values(): pool.sync(self.w3) - apy = generated_yield_pct(allocations, assets_and_pools, extra_metadata) - print(f"annualized yield: {(((1 + float(apy) / 1e18)**(365)) - 1) * 100}%") + apy = annualized_yield_pct(allocations, assets_and_pools, 604800, extra_metadata) + print(f"annualized yield: {(float(apy)/1e18) * 100}%") self.assertGreater(apy, 0) From ae1418680134ace45b6d1a83256ffd8971686cf3 Mon Sep 17 00:00:00 2001 From: Syeam Bin Abdullah Date: Fri, 15 Nov 2024 01:12:47 +0000 Subject: [PATCH 25/51] fix: update websocket-client to get around EOF ssl error --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 673fcf9..1a9cc9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wandb==0.17.0 loguru==0.7.0 -bittensor @ git+https://github.com/opentensor/bittensor@release/6.11.1 +bittensor==6.11.1 torch==2.0.1 typer==0.9.0 starlette==0.27.0 @@ -10,3 +10,4 @@ python-dotenv==1.0.1 pandas==2.2.2 matplotlib==3.9.0 gmpy2==2.2.1 +websocket-client @ git+https://github.com/websocket-client/websocket-client.git@asyncpong#egg=websocket-client From b31e3de1483365ce66522d349318bd956afbca21 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Fri, 15 Nov 2024 05:26:59 +0000 Subject: [PATCH 26/51] fix: morpho supply rate calculation --- sturdy/pools.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sturdy/pools.py b/sturdy/pools.py index ec74a8c..38ad2fd 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -1108,17 +1108,14 @@ def supply_rate(self, amount: int) -> int: allocated_assets = self.shares_to_assets_down( position.supplyShares, market.totalSupplyAssets, market.totalSupplyShares ) - current_assets.append(allocated_assets) + current_assets.append(allocated_assets * int(10**self._asset_decimals)) - curr_agg_apy = sum( - [(current_assets[i] * current_supply_apys[i]) // int(10**self._asset_decimals) for i in range(supply_queue_length)] - ) / sum(current_assets) - - return int( - (wei_mul(curr_agg_apy, self._total_supplied_assets) / (self._total_supplied_assets + total_asset_delta)) - * 10 ** (self._asset_decimals * 2) + curr_agg_apy = sum([current_assets[i] * current_supply_apys[i] for i in range(supply_queue_length)]) / sum( + current_assets ) + return int(curr_agg_apy * self._total_supplied_assets / (self._total_supplied_assets + total_asset_delta)) + class YearnV3Vault(ChainBasedPoolModel): pool_type: POOL_TYPES = Field(POOL_TYPES.YEARN_V3, const=True, description="type of pool") From b65c925c2d068846318105eafb58cfe65fe7bb06 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sat, 16 Nov 2024 03:17:59 +0000 Subject: [PATCH 27/51] chore: logging + msg change --- sturdy/base/validator.py | 2 +- sturdy/validator/reward.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sturdy/base/validator.py b/sturdy/base/validator.py index 3e52064..644c2f5 100644 --- a/sturdy/base/validator.py +++ b/sturdy/base/validator.py @@ -67,7 +67,7 @@ def __init__(self, config=None) -> None: # set web3 provider url w3_provider_url = os.environ.get("WEB3_PROVIDER_URL") if w3_provider_url is None: - raise ValueError("You must provide a valid web3 provider url as an organic validator!") + raise ValueError("You must provide a valid web3 provider url") self.w3 = Web3(Web3.HTTPProvider(w3_provider_url)) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 0808f62..3adcbe9 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -413,7 +413,7 @@ def get_rewards(self, active_allocation) -> tuple[list, dict]: axon_times[miner_uid] = miner_axon_time apys_and_allocations[miner_uid] = {"apy": miner_apy, "allocations": allocations} - print(f"yields and allocs: {apys_and_allocations}") + bt.logging.debug(f"yields and allocs: {apys_and_allocations}") # TODO: there may be a better way to go about this if len(miner_uids) < 1: From faeee24ee561ae2580038369b9efca12b7d4780c Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sat, 16 Nov 2024 09:21:30 +0000 Subject: [PATCH 28/51] feat: delete stale active allocs --- sturdy/validator/forward.py | 24 ++++++++++++++++-------- sturdy/validator/sql.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index b114a43..e0c54b2 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -29,7 +29,7 @@ from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards -from sturdy.validator.sql import get_active_allocs, get_db_connection, log_allocations +from sturdy.validator.sql import delete_stale_active_allocs, get_active_allocs, get_db_connection, log_allocations async def forward(self) -> Any: @@ -42,6 +42,12 @@ async def forward(self) -> Any: self (:obj:`bittensor.neuron.Neuron`): The neuron object which contains all the necessary state for the validator. """ + # delete stale active allocations after expiry time + bt.logging.debug("Purging stale active allocation requests") + with get_db_connection(self.config.db_dir) as conn: + rows_affected = delete_stale_active_allocs(conn) + bt.logging.debug(f"Purged {rows_affected} stale active allocation requests") + # initialize pools and assets # TODO: only sturdy silos and morpho vaults for now challenge_data = generate_challenge_data(self.w3) @@ -60,7 +66,6 @@ async def forward(self) -> Any: pools = assets_and_pools["pools"] metadata = get_metadata(pools, self.w3) - scoring_period = get_scoring_period() with get_db_connection(self.config.db_dir) as conn: @@ -75,6 +80,7 @@ async def forward(self) -> Any: scoring_period, ) + def get_metadata(pools: dict[str, ChainBasedPoolModel], w3: Web3) -> dict: metadata = {} for contract_addr, pool in pools.items(): @@ -89,17 +95,19 @@ def get_metadata(pools: dict[str, ChainBasedPoolModel], w3: Web3) -> dict: return metadata + def get_scoring_period(rng_gen: np.random.RandomState = None) -> int: if rng_gen is None: rng_gen = np.random.RandomState() return rng_gen.choice( - np.arange( - MIN_SCORING_PERIOD, - MAX_SCORING_PERIOD + SCORING_PERIOD_STEP, - SCORING_PERIOD_STEP, - ), - ) + np.arange( + MIN_SCORING_PERIOD, + MAX_SCORING_PERIOD + SCORING_PERIOD_STEP, + SCORING_PERIOD_STEP, + ), + ) + async def query_miner( self, diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 9a43bed..042534d 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -218,6 +218,21 @@ def get_active_allocs(conn: sqlite3.Connection, scoring_window: float = SCORING_ return [dict(row) for row in rows] +def delete_stale_active_allocs(conn: sqlite3.Connection, scoring_window: int = SCORING_WINDOW) -> int: + query = f""" + DELETE FROM {ACTIVE_ALLOCS} + WHERE scoring_period_end < ? + """ + ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + expiry_ts = ts_now - scoring_window + expiration_date = datetime.fromtimestamp(expiry_ts) # noqa: DTZ006 + + cur = conn.execute(query, [expiration_date]) + conn.commit() + + return cur.rowcount + + def get_miner_responses( conn: sqlite3.Connection, request_uid: str | None = None, From 5da917be6cb78a3f3bcf8d12d1a4e66ff029a3f7 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 17 Nov 2024 01:11:53 +0000 Subject: [PATCH 29/51] chore: remove pool_model_disc from morpho vault in registry --- sturdy/pool_registry/pool_registry.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py index e9c4fc4..cdc2781 100644 --- a/sturdy/pool_registry/pool_registry.py +++ b/sturdy/pool_registry/pool_registry.py @@ -44,22 +44,18 @@ "pools": { "0xd63070114470f685b75B74D60EEc7c1113d33a3D": { "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", "contract_address": "0xd63070114470f685b75B74D60EEc7c1113d33a3D", }, "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB": { "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", "contract_address": "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", }, "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458": { "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", "contract_address": "0x8eB67A509616cd6A7c1B3c8C21D48FF57df3d458", }, "0xdd0f28e19C1780eb6396170735D45153D261490d": { "pool_type": "MORPHO", - "pool_model_disc": "CHAIN", "contract_address": "0xdd0f28e19C1780eb6396170735D45153D261490d", }, } From 05a23087f4c084ed905b690aad09763ae96ef891 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 17 Nov 2024 03:33:11 +0000 Subject: [PATCH 30/51] feat: unit tests for sql funcs --- pyproject.toml | 2 +- sturdy/validator/sql.py | 6 +- .../validator/test_integration_validator.py | 3 - tests/unit/validator/test_sql.py | 349 ++++++++++++++++++ 4 files changed, 353 insertions(+), 7 deletions(-) create mode 100644 tests/unit/validator/test_sql.py diff --git a/pyproject.toml b/pyproject.toml index a95b13e..a5e11bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ indent-width = 4 target-version = "py311" [tool.ruff.lint] -ignore = ["NPY002", "F405", "F403", "E402", "D", "ANN001", "FBT001", "FBT002", "TD002", "TD003", "PLR", "C901", "BLE001", "ANN401", "N801", "EM101", "EM102", "TRY003", "S608", "FIX002", "N805", "N815", "N806", "PT009", "COM812", "S101", "SLF001", "T201"] +ignore = ["NPY002", "F405", "F403", "E402", "D", "ANN001", "FBT001", "FBT002", "TD002", "TD003", "PLR", "C901", "BLE001", "ANN401", "N801", "EM101", "EM102", "TRY003", "S608", "FIX002", "N805", "N815", "N806", "PT009", "COM812", "S101", "SLF001", "T201", "DTZ003"] select = ["ALL"] [tool.ruff.format] diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 042534d..37158ed 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -8,7 +8,7 @@ from fastapi.encoders import jsonable_encoder from sturdy.constants import DB_DIR, SCORING_WINDOW -from sturdy.protocol import AllocInfo, ChainBasedPoolModel +from sturdy.protocol import REQUEST_TYPES, AllocInfo, ChainBasedPoolModel BALANCE = "balance" KEY = "key" @@ -157,8 +157,8 @@ def log_allocations( assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], extra_metadata: dict, allocations: dict[str, AllocInfo], - axon_times: list, - request_type: REQUEST_TYPE, + axon_times: dict[str, float], + request_type: REQUEST_TYPES, scoring_period: int, ) -> None: ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index b3f3710..d6113b2 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -6,7 +6,6 @@ from pathlib import Path from unittest import IsolatedAsyncioTestCase -import bittensor as bt import numpy as np import torch from dotenv import load_dotenv @@ -64,7 +63,6 @@ def init_db(conn: sqlite3.Connection) -> None: conn.executescript(query) -# TODO: more comprehensive integration testing - with in-mem sql db and everythin' class TestValidator(IsolatedAsyncioTestCase): @classmethod def setUpClass(cls) -> None: @@ -276,7 +274,6 @@ async def test_get_rewards(self) -> None: sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] print(f"sorted rewards: {sorted_rewards}") - # bt.logging.debug(f"miner rewards: {rewards}") print(f"sim penalities: {validator.similarity_penalties}") # rewards should not all be the same diff --git a/tests/unit/validator/test_sql.py b/tests/unit/validator/test_sql.py new file mode 100644 index 0000000..9f9e601 --- /dev/null +++ b/tests/unit/validator/test_sql.py @@ -0,0 +1,349 @@ +from datetime import datetime, timedelta +import json +import sqlite3 +import unittest +from pathlib import Path + +from sturdy.pools import POOL_TYPES, PoolFactory, assets_pools_for_challenge_data +from sturdy.protocol import REQUEST_TYPES +from sturdy.validator.sql import ( + add_api_key, + delete_api_key, + get_all_api_keys, + get_all_logs_for_key, + get_api_key_info, + get_db_connection, + get_miner_responses, + get_request_info, + log_allocations, + log_request, + update_api_key_balance, + update_api_key_name, + update_api_key_rate_limit, +) + +TEST_DB = "test.db" + + +# TODO: place this in a seperate file? +def create_tables(conn: sqlite3.Connection) -> None: + query = """CREATE TABLE api_keys ( + key TEXT PRIMARY KEY, + name TEXT, + balance REAL, + rate_limit_per_minute INTEGER DEFAULT 60, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + + CREATE TABLE logs ( + key TEXT, + endpoint TEXT, + cost REAL, + balance REAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(key) REFERENCES api_keys(key) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS allocation_requests ( + request_uid TEXT PRIMARY KEY, + assets_and_pools TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE active_allocs ( + request_uid TEXT PRIMARY KEY, + scoring_period_end TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) + ); + + CREATE TABLE IF NOT EXISTS allocations ( + request_uid TEXT, + miner_uid TEXT, + allocation TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (request_uid, miner_uid), + FOREIGN KEY (request_uid) REFERENCES allocation_requests (request_uid) + ); + + -- This alter statement adds a new column to the allocations table if it exists + ALTER TABLE allocation_requests + ADD COLUMN request_type TEXT NOT NULL DEFAULT 1; + ALTER TABLE allocation_requests + ADD COLUMN metadata TEXT; + ALTER TABLE allocations + ADD COLUMN axon_time FLOAT NOT NULL DEFAULT 99999.0; -- large number for now""" + + conn.executescript(query) + + +class TestSQLFunctions(unittest.TestCase): + def setUp(self) -> None: + # purge sql db + path = Path(TEST_DB) + if path.exists(): + path.unlink() + # Create an in-memory SQLite database + with get_db_connection(TEST_DB) as conn: + create_tables(conn) + + def tearDown(self) -> None: + # purge sql db + path = Path(TEST_DB) + if path.exists(): + path.unlink() + + def test_add_and_get_api_key(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add an API key + add_api_key(conn, "test_key", 100.0, 60, "Test Key") + # Retrieve the API key information + info = get_api_key_info(conn, "test_key") + self.assertIsNotNone(info) + self.assertEqual(info["key"], "test_key") + self.assertEqual(info["balance"], 100.0) + self.assertEqual(info["rate_limit_per_minute"], 60) + self.assertEqual(info["name"], "Test Key") + + def test_update_api_key_balance(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add an API key + add_api_key(conn, "test_key", 100.0, 60, "Test Key") + # Update the balance + update_api_key_balance(conn, "test_key", 200.0) + # Retrieve the updated information + info = get_api_key_info(conn, "test_key") + self.assertEqual(info["balance"], 200.0) + + def test_update_api_key_rate_limit(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add an API key + add_api_key(conn, "test_key", 100.0, 60, "Test Key") + # Update the rate limit + update_api_key_rate_limit(conn, "test_key", 120) + # Retrieve the updated information + info = get_api_key_info(conn, "test_key") + self.assertEqual(info["rate_limit_per_minute"], 120) + + def test_update_api_key_name(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add an API key + add_api_key(conn, "test_key", 100.0, 60, "Test Key") + # Update the name + update_api_key_name(conn, "test_key", "Updated Test Key") + # Retrieve the updated information + info = get_api_key_info(conn, "test_key") + self.assertEqual(info["name"], "Updated Test Key") + + def test_delete_api_key(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add an API key + add_api_key(conn, "test_key", 100.0, 60, "Test Key") + # Delete the API key + delete_api_key(conn, "test_key") + # Attempt to retrieve the deleted key + info = get_api_key_info(conn, "test_key") + self.assertIsNone(info) + + def test_get_all_api_keys(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add multiple API keys + add_api_key(conn, "key1", 100.0, 60, "Key 1") + add_api_key(conn, "key2", 200.0, 120, "Key 2") + # Retrieve all API keys + keys = get_all_api_keys(conn) + self.assertEqual(len(keys), 2) + self.assertEqual(keys[0]["key"], "key1") + self.assertEqual(keys[1]["key"], "key2") + + def test_log_request_and_get_logs(self) -> None: + with get_db_connection(TEST_DB) as conn: + # Add an API key + add_api_key(conn, "test_key", 100.0, 60, "Test Key") + # Log a request + api_key_info = get_api_key_info(conn, "test_key") + log_request(conn, api_key_info, "/test_endpoint", 1.0) + # Retrieve logs for the API key + logs = get_all_logs_for_key(conn, "test_key") + self.assertEqual(len(logs), 1) + self.assertEqual(logs[0]["key"], "test_key") + self.assertEqual(logs[0]["endpoint"], "/test_endpoint") + self.assertEqual(logs[0]["cost"], 1.0) + + def test_get_db_connection(self) -> None: + # Test the get_db_connection function + with get_db_connection(TEST_DB) as conn: + self.assertIsNotNone(conn) + # Ensure tables are created + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + self.assertGreater(len(tables), 0) + + def test_log_allocations(self) -> None: + with get_db_connection(TEST_DB) as conn: + request_uid = "sturdyrox" + + selected_entry = { + "user_address": "0xcFB23D05f32eA0BE0dBb5078d189Cca89688945E", + "assets_and_pools": { + "total_assets": 69420, + "pools": { + "0x0669091F451142b3228171aE6aD794cF98288124": { + "pool_type": "STURDY_SILO", + "contract_address": "0x0669091F451142b3228171aE6aD794cF98288124", + }, + "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D": { + "pool_type": "STURDY_SILO", + "contract_address": "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D", + }, + }, + }, + } + + allocations = { + "0": { + "0x0669091F451142b3228171aE6aD794cF98288124": 3, + "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D": 7, + }, + "1": { + "0x0669091F451142b3228171aE6aD794cF98288124": 2, + "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D": 8, + }, + "2": { + "0x0669091F451142b3228171aE6aD794cF98288124": 6, + "0xFa68707be4b58FB9F10748E30e25A15113EdEE1D": 4, + }, + } + + assets_and_pools = selected_entry["assets_and_pools"] + + log_allocations( + conn, + request_uid, + assets_and_pools, + extra_metadata={"yo": "wassup"}, + allocations=allocations, + axon_times={"0": 6.9, "1": 4.2, "2": 1.0}, + request_type=REQUEST_TYPES.SYNTHETIC, + scoring_period=69, + ) + + # Validate `allocation_requests` table + cur = conn.execute("SELECT * FROM allocation_requests WHERE request_uid = ?", (request_uid,)) + allocation_request = dict(cur.fetchone()) + self.assertIsNotNone(allocation_request) + self.assertEqual(allocation_request["request_uid"], request_uid) + self.assertEqual(allocation_request["metadata"], '{"yo":"wassup"}') + self.assertEqual(allocation_request["request_type"], str(int(REQUEST_TYPES.SYNTHETIC))) + + # Validate `active_allocs` table + cur = conn.execute("SELECT * FROM active_allocs WHERE request_uid = ?", (request_uid,)) + active_alloc = cur.fetchone() + self.assertIsNotNone(active_alloc) + self.assertEqual(active_alloc["request_uid"], request_uid) + + # Validate `allocations` table + cur = conn.execute("SELECT * FROM allocations WHERE request_uid = ?", (request_uid,)) + allocation_rows = cur.fetchall() + self.assertEqual(len(allocation_rows), len(allocations)) + for miner_uid, miner_allocation in allocations.items(): + for pool_id, allocation_value in miner_allocation.items(): + row = next( + ( + r + for r in allocation_rows + if r["miner_uid"] == miner_uid and json.loads(r["allocation"]).get(pool_id) == allocation_value + ), + None, + ) + self.assertIsNotNone(row) + self.assertEqual(row["request_uid"], request_uid) + self.assertIn(pool_id, row["allocation"]) + self.assertEqual(json.loads(row["allocation"])[pool_id], allocation_value) + + +class TestMinerResponseRequestInfo(unittest.TestCase): + def setUp(self) -> None: + # Initialize an in-memory SQLite database + self.conn = sqlite3.connect(":memory:") + self.conn.row_factory = sqlite3.Row + create_tables(self.conn) + + # Seed test data for allocations + self.request_uid = "test_request_1" + self.miner_uid = "miner_1" + created_at = datetime.utcnow() + self.conn.execute( + "INSERT INTO allocations (request_uid, miner_uid, allocation, created_at, axon_time) VALUES (?, ?, json(?), ?, ?)", + (self.request_uid, self.miner_uid, '{"pool_1": 100}', created_at, 1.2), + ) + self.conn.execute( + "INSERT INTO allocations (request_uid, miner_uid, allocation, created_at, axon_time) VALUES (?, ?, json(?), ?, ?)", + (self.request_uid, "miner_2", '{"pool_2": 200}', created_at + timedelta(minutes=1), 2.3), + ) + + # Seed test data for allocation requests + self.conn.execute( + "INSERT INTO allocation_requests (request_uid, assets_and_pools, created_at, request_type, metadata) VALUES (?, json(?), ?, ?, json(?))", + ( + self.request_uid, + '{"asset": {"pool": "data"}}', + created_at, + "TEST", + '{"meta": "data"}', + ), + ) + self.conn.commit() + + def tearDown(self) -> None: + self.conn.close() + + def test_get_miner_responses_with_request_uid(self) -> None: + responses = get_miner_responses(self.conn, request_uid=self.request_uid) + self.assertEqual(len(responses), 2) + self.assertEqual(responses[0]["miner_uid"], "miner_1") + self.assertEqual(responses[0]["request_uid"], self.request_uid) + self.assertEqual(responses[1]["miner_uid"], "miner_2") + + def test_get_miner_responses_with_miner_uid(self) -> None: + responses = get_miner_responses(self.conn, miner_uid="miner_2") + self.assertEqual(len(responses), 1) + self.assertEqual(responses[0]["miner_uid"], "miner_2") + self.assertEqual(json.loads(responses[0]["allocation"])["pool_2"], 200) + + def test_get_miner_responses_with_time_range(self) -> None: + now = datetime.utcnow() + from_ts = int((now - timedelta(minutes=5)).timestamp() * 1000) + to_ts = int((now + timedelta(minutes=5)).timestamp() * 1000) + + responses = get_miner_responses(self.conn, from_ts=from_ts, to_ts=to_ts) + self.assertEqual(len(responses), 2) + + def test_get_request_info_with_request_uid(self) -> None: + info = get_request_info(self.conn, request_uid=self.request_uid) + self.assertEqual(len(info), 1) + self.assertEqual(info[0]["request_uid"], self.request_uid) + self.assertEqual(info[0]["request_type"], "TEST") + self.assertEqual(json.loads(info[0]["metadata"])["meta"], "data") + + def test_get_request_info_with_time_range(self) -> None: + now = datetime.utcnow() + from_ts = int((now - timedelta(minutes=5)).timestamp() * 1000) + to_ts = int((now + timedelta(minutes=5)).timestamp() * 1000) + + info = get_request_info(self.conn, from_ts=from_ts, to_ts=to_ts) + self.assertEqual(len(info), 1) + self.assertEqual(info[0]["request_uid"], self.request_uid) + + def test_get_request_info_no_results(self) -> None: + from_ts = int((datetime.utcnow() + timedelta(days=1)).timestamp() * 1000) + to_ts = int((datetime.utcnow() + timedelta(days=2)).timestamp() * 1000) + + info = get_request_info(self.conn, from_ts=from_ts, to_ts=to_ts) + self.assertEqual(len(info), 0) + + +if __name__ == "__main__": + unittest.main() From cc38fa88f5cb4459a66a5c9001bc99dbfa277d36 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 17 Nov 2024 03:34:52 +0000 Subject: [PATCH 31/51] chore: ruff format --- sturdy/base/validator.py | 7 +------ sturdy/constants.py | 2 +- sturdy/utils/misc.py | 2 ++ tests/unit/validator/test_pool_models.py | 5 +---- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/sturdy/base/validator.py b/sturdy/base/validator.py index 644c2f5..1c06f13 100644 --- a/sturdy/base/validator.py +++ b/sturdy/base/validator.py @@ -411,12 +411,7 @@ def save_state(self) -> None: # Save the state of the validator to file. torch.save( - { - "step": self.step, - "scores": self.scores, - "hotkeys": self.hotkeys, - "last_query_block": self.last_query_block - }, + {"step": self.step, "scores": self.scores, "hotkeys": self.hotkeys, "last_query_block": self.last_query_block}, self.config.neuron.full_path + "/state.pt", ) diff --git a/sturdy/constants.py b/sturdy/constants.py index 1064e71..1d93993 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -41,7 +41,7 @@ TOTAL_ALLOC_THRESHOLD = 0.98 SIMILARITY_THRESHOLD = 0.01 # similarity threshold for plagiarism checking -DB_DIR = "validator_database.db" # default validator database dir +DB_DIR = "validator_database.db" # default validator database dir # The following constants are for different pool models # Aave diff --git a/sturdy/utils/misc.py b/sturdy/utils/misc.py index 2f81ee3..e562843 100644 --- a/sturdy/utils/misc.py +++ b/sturdy/utils/misc.py @@ -42,11 +42,13 @@ def time_diff_seconds(start: str, end: str, format_str: str = "%Y-%m-%d %H:%M:%S end_datetime = datetime.strptime(end, format_str).replace(tzinfo=timezone.utc) # noqa: UP017 return (end_datetime - start_datetime).seconds + def get_scoring_period_length(active_allocation: dict) -> int: scoring_period_start = active_allocation["created_at"] scoring_period_end = active_allocation["scoring_period_end"] return time_diff_seconds(scoring_period_start, scoring_period_end) + # rand range but float def randrange_float( start, diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 2afcdca..fb53504 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -943,10 +943,7 @@ def test_sync(self) -> None: # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: print("----==== test_supply_rate_increase_alloc ====----") - pool = AaveV3DefaultInterestRateV2Pool( - contract_address=self.atoken_address, - user_address=self.account.address - ) + pool = AaveV3DefaultInterestRateV2Pool(contract_address=self.atoken_address, user_address=self.account.address) # sync pool params pool.sync(web3_provider=self.w3) From 2d195b15ffb12b23ae4c72053fd467e89750dc92 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 17 Nov 2024 05:12:36 +0000 Subject: [PATCH 32/51] feat: more integration tests + small patches --- sturdy/validator/reward.py | 2 +- .../validator/test_integration_validator.py | 143 +++++++++++++++++- tests/unit/validator/test_reward_helpers.py | 8 - tests/unit/validator/test_sql.py | 3 +- 4 files changed, 140 insertions(+), 16 deletions(-) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 3adcbe9..454e71f 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -91,7 +91,7 @@ def normalize_squared(apys_and_allocations: AllocationsDict, epsilon: float = 1e if len(raw_apys) <= 1: return torch.zeros(len(raw_apys)) - apys = torch.tensor(list(raw_apys.values())).to(torch.float32) + apys = torch.tensor(list(raw_apys.values()), dtype=torch.float32) squared = torch.pow(apys, 2) diff --git a/tests/integration/validator/test_integration_validator.py b/tests/integration/validator/test_integration_validator.py index d6113b2..7dafb69 100644 --- a/tests/integration/validator/test_integration_validator.py +++ b/tests/integration/validator/test_integration_validator.py @@ -66,7 +66,7 @@ def init_db(conn: sqlite3.Connection) -> None: class TestValidator(IsolatedAsyncioTestCase): @classmethod def setUpClass(cls) -> None: - np.random.seed(69) # noqa: NPY002 + np.random.seed(69) cls.config = { "mock": True, "wandb": {"off": True}, @@ -105,6 +105,8 @@ def setUpClass(cls) -> None: cls.contract_addresses: list[str] = list(cls.assets_and_pools["pools"].keys()) # type: ignore[] + cls.used_netuids = [] + @classmethod def tearDownClass(cls) -> None: # run this after tests to restore original forked state @@ -131,7 +133,12 @@ def setUp(self) -> None: self.snapshot_id = self.w3.provider.make_request("evm_snapshot", []) # type: ignore[] print(f"snapshot id: {self.snapshot_id}") - self.validator = Validator(config=self.config) + netuid = np.random.randint(69, 420) + self.used_netuids.append(netuid) + conf = copy(self.config) + conf["netuid"] = netuid + + self.validator = Validator(config=conf) self.validator.w3 = self.w3 assert self.validator.w3.is_connected() @@ -166,7 +173,6 @@ async def test_get_rewards(self) -> None: print(f"tables: {tables}") assets_and_pools = copy(self.assets_and_pools) - allocations = copy(self.allocations) validator = self.validator validator.dendrite = MockDendrite(wallet=validator.wallet, custom_allocs=True) @@ -196,7 +202,6 @@ async def test_get_rewards(self) -> None: for response in responses: # TODO: is this necessary? - # self.assertEqual(response.assets_and_pools, self.assets_and_pools) self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) # Log the results for monitoring purposes. @@ -206,7 +211,6 @@ async def test_get_rewards(self) -> None: pools = assets_and_pools["pools"] metadata = get_metadata(pools, validator.w3) - # axon_times = get_response_times(uids=active_uids, responses=responses, timeout=QUERY_TIMEOUT) # scoring period is ~12 hours scoring_period = 43200 @@ -283,6 +287,135 @@ async def test_get_rewards(self) -> None: freezer.stop() + async def test_get_rewards_punish(self) -> None: + print("----==== test_get_rewards_punish ====----") + + freezer = freeze_time("2024-01-11 00:00:00.124513") + freezer.start() + + request_uuid = str(uuid.uuid4()).replace("-", "") + + with get_db_connection(self.validator.config.db_dir, True) as conn: + cur = conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [dict(t) for t in cur.fetchall()] + print(f"tables: {tables}") + + assets_and_pools = copy(self.assets_and_pools) + allocations = copy(self.allocations) + + validator = self.validator + validator.dendrite = MockDendrite(wallet=validator.wallet) + + # ==== + + active_uids = [str(uid) for uid in range(validator.metagraph.n.item()) if validator.metagraph.axons[uid].is_serving] + + np.random.shuffle(active_uids) + + print(f"active_uids: {active_uids}") + + synapse = AllocateAssets( + request_type=REQUEST_TYPES.SYNTHETIC, + assets_and_pools=assets_and_pools, + user_address=self.user_address, + allocations=allocations, + ) + + # query all miners + responses = await query_multiple_miners( + validator, + synapse, + active_uids, + ) + + allocations = {uid: responses[idx].allocations for idx, uid in enumerate(active_uids)} # type: ignore[] + + for response in responses: + # TODO: is this necessary? + self.assertLessEqual(sum(response.allocations.values()), assets_and_pools["total_assets"]) + + # Log the results for monitoring purposes. + print(f"Assets and pools: {synapse.assets_and_pools}") + print(f"Received allocations (uid -> allocations): {allocations}") + + pools = assets_and_pools["pools"] + metadata = get_metadata(pools, validator.w3) + + # scoring period is ~12 hours + scoring_period = 43200 + + axon_times, filtered_allocs = filter_allocations( + self, + query=validator.step, + uids=active_uids, + responses=responses, + assets_and_pools=assets_and_pools, + ) + + # log allocations + with get_db_connection(validator.config.db_dir) as conn: + log_allocations( + conn, + request_uuid, + assets_and_pools, + metadata, + filtered_allocs, + axon_times, + REQUEST_TYPES.SYNTHETIC, + scoring_period, + ) + + freezer.stop() + + # fast forward ~12 hrs + + freezer = freeze_time("2024-01-11 12:01:00.136136") + freezer.start() + + validator.w3.provider.make_request( + "hardhat_reset", # type: ignore[] + [ + { + "forking": { + "jsonRpcUrl": EXTERNAL_WEB3_PROVIDER_URL, + "blockNumber": 21150770, + }, + }, + ], + ) + + curr_pools = assets_and_pools["pools"] + for pool in curr_pools.values(): + pool.sync(validator.w3) + + # score previously suggested miner allocations based on how well they are performing now + # get all the request ids for the pools we should be scoring from the db + active_alloc_rows = [] + with get_db_connection(validator.config.db_dir, True) as conn: + active_alloc_rows = get_active_allocs(conn) + + print(f"Active allocs: {active_alloc_rows}") + + with get_db_connection(validator.config.db_dir, True) as conn: + all_requests = get_request_info(conn) + print(f"all requests: {all_requests}") + + for active_alloc in active_alloc_rows: + # calculate rewards for previous active allocations + miner_uids, rewards = get_rewards(validator, active_alloc) + + rewards_dict = {active_uids[k]: v for k, v in enumerate(list(rewards))} + sorted_rewards = dict(sorted(rewards_dict.items(), key=lambda item: item[1], reverse=True)) # type: ignore[] + + print(f"sorted rewards: {sorted_rewards}") + print(f"sim penalities: {validator.similarity_penalties}") + + # rewards should not all be the same + to_compare = torch.zeros_like(rewards) + self.assertTrue(torch.equal(rewards, to_compare)) + + freezer.stop() + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 8ed7e81..cdd2a5b 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -780,10 +780,6 @@ def test_calculate_apy_sturdy(self) -> None: pool.sync(self.w3) extra_metadata[contract_address] = pool._share_price - # move forwards in time, back to "present" - # TODO: why doesnt the following work? - # self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] - self.w3.provider.make_request( "hardhat_reset", # type: ignore[] [ @@ -849,10 +845,6 @@ def test_calculate_apy_aave(self) -> None: pool.sync(self.w3) extra_metadata[contract_address] = pool._normalized_income - # move forwards in time, back to "present" - # TODO: why doesnt the following work? - # self.w3.provider.make_request("evm_revert", self.snapshot_id) # type: ignore[] - self.w3.provider.make_request( "hardhat_reset", # type: ignore[] [ diff --git a/tests/unit/validator/test_sql.py b/tests/unit/validator/test_sql.py index 9f9e601..6740010 100644 --- a/tests/unit/validator/test_sql.py +++ b/tests/unit/validator/test_sql.py @@ -1,10 +1,9 @@ -from datetime import datetime, timedelta import json import sqlite3 import unittest +from datetime import datetime, timedelta from pathlib import Path -from sturdy.pools import POOL_TYPES, PoolFactory, assets_pools_for_challenge_data from sturdy.protocol import REQUEST_TYPES from sturdy.validator.sql import ( add_api_key, From 267f9cf837cf2ee1103f2675557b7881fc4ef8b7 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 17 Nov 2024 07:18:51 +0000 Subject: [PATCH 33/51] chore: update docs + cleanup --- README.md | 133 +++++++++++++++++++----------------- docs/validator.md | 12 ++-- sturdy/validator/forward.py | 3 + sturdy/validator/reward.py | 5 +- sturdy/validator/sql.py | 6 +- 5 files changed, 86 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 97614f1..6258da7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ There are three core files. 1. `sturdy/protocol.py`: Contains the definition of the protocol used by subnet miners and subnet validators. At the moment it only has one kind of synapse - `AllocateAssets` - which contains the inputs (`assets_and_pools`) validators need to send to miners to generate return - `allocations` for. See `generate_assets_in_pools()` in [pools.py](./sturdy/pools.py) to see how + `allocations` for. See `generate_challenge_data()` in [pools.py](./sturdy/pools.py) to see how assets and pools are defined. 2. `neurons/miner.py`: Script that defines the subnet miner's behavior, i.e., how the subnet miner responds to requests from subnet validators. @@ -53,66 +53,74 @@ There are three core files. used for generating challenge data in [pools.py](./sturdy/pools.py) used for synthetic requests. The selection of different assets and pools which can be used in such requests are defined in the [pool registry](./sturdy/pool_registry/pool_registry.py), and are all based on pools which are real and do indeed exist on-chain (i.e. on the Ethereum Mainnet): ```python - def generate_challenge_data( - web3_provider: Web3, - rng_gen: np.random.RandomState = np.random.RandomState(), - ) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools - selected_entry = POOL_REGISTRY[rng_gen.choice(list(POOL_REGISTRY.keys()))] - bt.logging.debug(f"Selected pool registry entry: {selected_entry}") - - return assets_pools_for_challenge_data(selected_entry, web3_provider) - - - def assets_pools_for_challenge_data( - selected_entry, web3_provider: Web3 - ) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools - challenge_data = {} - - selected_assets_and_pools = selected_entry["assets_and_pools"] - selected_pools = selected_assets_and_pools["pools"] - user_address = selected_entry["user_address"] - - pool_list = [] - - for pool_dict in selected_pools.values(): - pool = PoolFactory.create_pool( - pool_type=POOL_TYPES._member_map_[pool_dict["pool_type"]], - user_address=user_address, - contract_address=pool_dict["contract_address"], - ) - pool_list.append(pool) - - pools = {str(pool.contract_address): pool for pool in pool_list} - - # we assume that the user address is the same across pools (valid) - # and also that the asset contracts are the same across said pools - first_pool = pool_list[0] - total_assets = 0 - - match first_pool.pool_type: - case POOL_TYPES.STURDY_SILO: - first_pool.sync(web3_provider) - total_assets = first_pool._user_asset_balance - case _: - pass - - for pool in pools.values(): - pool.sync(web3_provider) - total_asset = 0 - match pool.pool_type: - case POOL_TYPES.STURDY_SILO: - total_asset += pool._user_deposits - case _: - pass - - total_assets += total_asset - - challenge_data["assets_and_pools"] = {} - challenge_data["assets_and_pools"]["pools"] = pools - challenge_data["assets_and_pools"]["total_assets"] = total_assets - challenge_data["user_address"] = user_address - - return challenge_data + def generate_challenge_data( + web3_provider: Web3, + rng_gen: np.random.RandomState = np.random.RandomState(), # noqa: B008 + ) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools + selected_entry = POOL_REGISTRY[rng_gen.choice(list(POOL_REGISTRY.keys()))] + bt.logging.debug(f"Selected pool registry entry: {selected_entry}") + + return assets_pools_for_challenge_data(selected_entry, web3_provider) + + + def assets_pools_for_challenge_data( + selected_entry, web3_provider: Web3 + ) -> dict[str, dict[str, ChainBasedPoolModel] | int]: # generate pools + challenge_data = {} + + selected_assets_and_pools = selected_entry["assets_and_pools"] + selected_pools = selected_assets_and_pools["pools"] + global_user_address = selected_entry.get("user_address", None) + + pool_list = [] + + for pool_dict in selected_pools.values(): + user_address = pool_dict.get("user_address", None) + pool = PoolFactory.create_pool( + pool_type=POOL_TYPES._member_map_[pool_dict["pool_type"]], + user_address=global_user_address if user_address is None else user_address, + contract_address=pool_dict["contract_address"], + ) + pool_list.append(pool) + + pools = {str(pool.contract_address): pool for pool in pool_list} + + # we assume that the user address is the same across pools (valid) + # and also that the asset contracts are the same across said pools + total_assets = selected_entry.get("total_assets", None) + + if total_assets is None: + total_assets = 0 + first_pool = pool_list[0] + first_pool.sync(web3_provider) + match first_pool.pool_type: + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET, POOL_TYPES.MORPHO): + total_assets = first_pool._user_asset_balance + case _: + pass + + for pool in pools.values(): + pool.sync(web3_provider) + total_asset = 0 + match pool.pool_type: + case POOL_TYPES.STURDY_SILO: + total_asset += pool._user_deposits + case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): + total_asset += pool._user_deposits + case POOL_TYPES.MORPHO: + total_asset += pool._user_deposits + case _: + pass + + total_assets += total_asset + + challenge_data["assets_and_pools"] = {} + challenge_data["assets_and_pools"]["pools"] = pools + challenge_data["assets_and_pools"]["total_assets"] = total_assets + if global_user_address is not None: + challenge_data["user_address"] = global_user_address + + return challenge_data ``` Validators can optionally run an API server and sell their bandwidth to outside users to send their own pools (organic requests) to the subnet. For more information on this process - please read @@ -132,8 +140,7 @@ There are three core files. not perceived as being original. If miners fail to respond in ~45 seconds after receiving the request they are scored poorly. The best allocating miner will receive the most emissions. For more information on how - miners are rewarded - please see - [reward.py](sturdy/validator/reward.py). A diagram is provided below highlighting the interactions that takes place within + miners are rewarded - please see [forward.py](sturdy/validator/forward.py), [reward.py](sturdy/validator/reward.py), and [validator.py](neurons/validator.py). A diagram is provided below highlighting the interactions that takes place within the subnet when processing synthetic and organic requests:

diff --git a/docs/validator.md b/docs/validator.md index b1abd35..b9e4564 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -53,7 +53,7 @@ We recommend using a third party service to connect to an RPC to perform on-chai ## Synthetic Validator -This is the most simple of the two. Synthetic validators generate dummy (fake) pools to send to miners to challenge them. To run a synthetic validator, run: +This is the most simple of the two. Synthetic validators generate synthetic requests to send to miners to challenge them. To run a synthetic validator, run: #### Starting the validator - without PM2 ```bash python3 neurons/validator.py --netuid NETUID --subtensor.network NETWORK --wallet.name NAME --wallet.hotkey HOTKEY --logging.trace --axon.port PORT --organic False @@ -235,7 +235,7 @@ Some annotations are provided below to further help understand the request forma "total_assets": 548568963376234830607950, # total assets available to a miner to allocate "pools": { # pools available to output allocations for "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227": { # address used to get relevant info about the the pool - "pool_type": "STURDY_SILO", # if this is a synthetic or chain (organic) pool + "pool_type": "STURDY_SILO", # type of pool (i.e sturdy silo, aave pool, yearn vault, etc.) "contract_address": "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227" # address used to get relevant info about the the pool }, ``` @@ -246,8 +246,8 @@ And the corresponding response(example) format from the subnet: "request_uuid":"1e09d3f1ce574921bd13a2461607f5fe", "allocations":{ "1":{ # miner uid - "apy":62133011236204113, # apy of miner's allocations in 18 decimal precision because the asset has the same precision. - "allocations":{ # allocations to pools in wei + "rank":1, # rank of the miner based on past performance + "allocations":{ # allocations to pools "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227":114864688949643874140160, "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b":1109027125282399872, "0x26fe402A57D52c8a323bb6e09f06489C8216aC88":71611128603622265323520, @@ -255,7 +255,7 @@ And the corresponding response(example) format from the subnet: } }, "4":{ - "apy":61332661325287823, + "rank":2, "allocations":{ "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227":119201178628424617426944, "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b":1290874337673458688, @@ -264,7 +264,7 @@ And the corresponding response(example) format from the subnet: } }, "2":{ - "apy":31168293423379011, + "rank":3, "allocations":{ "0x6311fF24fb15310eD3d2180D3d0507A21a8e5227":45592862828746122461184, "0x200723063111f9f8f1d44c0F30afAdf0C0b1a04b":172140896186699296, diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index e0c54b2..d53201c 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -208,12 +208,15 @@ async def query_and_score_miners( sorted_indices = [idx for idx, val in sorted(enumerate(self.scores), key=lambda k: k[1], reverse=True)] sorted_allocs = {} + rank = 1 for idx in sorted_indices: alloc = filtered_allocs.get(str(idx), None) if alloc is None: continue + alloc["rank"] = rank sorted_allocs[str(idx)] = alloc + rank += 1 bt.logging.debug(f"sorted allocations: {sorted_allocs}") diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 454e71f..b4723a6 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -320,6 +320,7 @@ def filter_allocations( filtered_allocs = {} axon_times = get_response_times(uids=uids, responses=responses, timeout=QUERY_TIMEOUT) + cheaters = [] for response_idx, response in enumerate(responses): allocations = response.allocations @@ -333,7 +334,7 @@ def filter_allocations( # score response very low if miner is cheating somehow or returns allocations with incorrect format if cheating: miner_uid = uids[response_idx] - bt.logging.warning(f"CHEATER DETECTED | UID {miner_uid}") + cheaters.append(miner_uid) continue # used to filter out miners who timed out @@ -344,6 +345,8 @@ def filter_allocations( "allocations": response.allocations, } + bt.logging.warning(f"CHEATERS DETECTED: {cheaters}") + curr_filtered_allocs = dict(sorted(filtered_allocs.items(), key=lambda item: int(item[0]))) sorted_axon_times = dict(sorted(axon_times.items(), key=lambda item: item[1])) diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 37158ed..001aaf6 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -161,7 +161,7 @@ def log_allocations( request_type: REQUEST_TYPES, scoring_period: int, ) -> None: - ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + ts_now = datetime.utcnow().timestamp() challenge_end = ts_now + scoring_period scoring_period_end = datetime.fromtimestamp(challenge_end) # noqa: DTZ006 datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 @@ -207,7 +207,7 @@ def get_active_allocs(conn: sqlite3.Connection, scoring_window: float = SCORING_ WHERE scoring_period_end >= ? AND scoring_period_end < ? """ - ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + ts_now = datetime.utcnow().timestamp() window_ts = ts_now - scoring_window datetime_now = datetime.fromtimestamp(ts_now) # noqa: DTZ006 window_datetime = datetime.fromtimestamp(window_ts) # noqa: DTZ006 @@ -223,7 +223,7 @@ def delete_stale_active_allocs(conn: sqlite3.Connection, scoring_window: int = S DELETE FROM {ACTIVE_ALLOCS} WHERE scoring_period_end < ? """ - ts_now = datetime.utcnow().timestamp() # noqa: DTZ003 + ts_now = datetime.utcnow().timestamp() expiry_ts = ts_now - scoring_window expiration_date = datetime.fromtimestamp(expiry_ts) # noqa: DTZ006 From 62b07509c0befb0eed7266cf68a605df4f6fa008 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Sun, 17 Nov 2024 12:17:59 +0000 Subject: [PATCH 34/51] fix: fixed aave pool model tests --- tests/unit/validator/test_pool_models.py | 98 ++++-------------------- 1 file changed, 16 insertions(+), 82 deletions(-) diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index fb53504..d7169bf 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -145,9 +145,9 @@ def test_supply_rate_alloc(self) -> None: apy_before = Web3.to_wei(reserve_data.currentLiquidityRate / 1e27, "ether") print(f"apy before supplying: {apy_before}") - # calculate predicted future supply rate after supplying 10000 ETH - apy_after = pool.supply_rate(int(10000e18)) - print(f"apy after supplying 10000 ETH: {apy_after}") + # calculate predicted future supply rate after supplying 2000000 ETH + apy_after = pool.supply_rate(int(2000000e18)) + print(f"apy after supplying 2000000 ETH: {apy_after}") self.assertNotEqual(apy_after, 0) self.assertLess(apy_after, apy_before) @@ -854,28 +854,19 @@ def setUpClass(cls) -> None: ], ) - cls.atoken_address = "0x4d5F47FA6A74757f35C14fD3a6Ef8E3C9BC514E8" + # spark dai + cls.atoken_address = "0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B" # Create a funded account for testing - cls.account = Account.create() + # cls.account = Account.create() + cls.account_address = "0x0Fd6abA4272a96Bb8CcbbA69B825075cb2047D1D" # spDai holder (~17.5k spDai at time of writing) cls.w3.eth.send_transaction( { - "to": cls.account.address, + "to": cls.account_address, "from": cls.w3.eth.accounts[0], "value": cls.w3.to_wei(200000, "ether"), } ) - weth_abi_file_path = Path(__file__).parent / "../../../sturdy/abi/IWETH.json" - weth_abi_file = weth_abi_file_path.open() - weth_abi = json.load(weth_abi_file) - weth_abi_file.close() - - weth_contract = cls.w3.eth.contract(abi=weth_abi) - cls.weth_contract = retry_with_backoff( - weth_contract, - address=Web3.to_checksum_address("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), - ) - cls.snapshot_id = cls.w3.provider.make_request("evm_snapshot", []) # type: ignore[] print(f"snapshot id: {cls.snapshot_id}") @@ -908,7 +899,7 @@ def tearDown(self) -> None: def test_pool_contract(self) -> None: print("----==== test_pool_contract ====----") # we call the aave3 weth atoken proxy contract in this example - pool = AaveV3DefaultInterestRateV2Pool( + pool = AaveV3RateTargetBaseInterestRatePool( contract_address=self.atoken_address, ) @@ -922,7 +913,7 @@ def test_pool_contract(self) -> None: # TODO: test syncing after time travel def test_sync(self) -> None: print("----==== test_sync ====----") - pool = AaveV3DefaultInterestRateV2Pool( + pool = AaveV3RateTargetBaseInterestRatePool( contract_address=self.atoken_address, ) @@ -943,7 +934,7 @@ def test_sync(self) -> None: # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: print("----==== test_supply_rate_increase_alloc ====----") - pool = AaveV3DefaultInterestRateV2Pool(contract_address=self.atoken_address, user_address=self.account.address) + pool = AaveV3RateTargetBaseInterestRatePool(contract_address=self.atoken_address, user_address=self.account_address) # sync pool params pool.sync(web3_provider=self.w3) @@ -953,82 +944,25 @@ def test_supply_rate_alloc(self) -> None: apy_before = Web3.to_wei(reserve_data.currentLiquidityRate / 1e27, "ether") print(f"apy before supplying: {apy_before}") - # calculate predicted future supply rate after supplying 10000 ETH - apy_after = pool.supply_rate(int(1000e18)) - print(f"apy after supplying 10000 ETH: {apy_after}") + # calculate predicted future supply rate after supplying 100000 DAI + apy_after = pool.supply_rate(int(100000e18)) + print(f"apy after supplying 100000 DAI: {apy_after}") self.assertNotEqual(apy_after, 0) self.assertLess(apy_after, apy_before) def test_supply_rate_decrease_alloc(self) -> None: print("----==== test_supply_rate_decrease_alloc ====----") - pool = AaveV3RateTargetBaseInterestRatePool(contract_address=self.atoken_address, user_address=self.account.address) + pool = AaveV3RateTargetBaseInterestRatePool(contract_address=self.atoken_address, user_address=self.account_address) # sync pool params pool.sync(web3_provider=self.w3) - tx = self.weth_contract.functions.deposit().build_transaction( - { - "from": self.w3.to_checksum_address(self.account.address), - "gas": 100000, - "gasPrice": self.w3.eth.gas_price, - "nonce": self.w3.eth.get_transaction_count(self.account.address), - "value": self.w3.to_wei(10000, "ether"), - } - ) - - signed_tx = self.w3.eth.account.sign_transaction(transaction_dict=tx, private_key=self.account.key) - - # Send the transaction - tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) - print(f"weth deposit tx hash: {tx_hash}") - - # check if we received some weth - weth_balance = self.weth_contract.functions.balanceOf(self.account.address).call() - self.assertGreaterEqual(int(weth_balance), self.w3.to_wei(10000, "ether")) - - # approve aave pool to use weth - tx = self.weth_contract.functions.approve(pool._pool_contract.address, self.w3.to_wei(1e9, "ether")).build_transaction( - { - "from": self.w3.to_checksum_address(self.account.address), - "gas": 1000000, - "gasPrice": self.w3.eth.gas_price, - "nonce": self.w3.eth.get_transaction_count(self.account.address), - } - ) - - signed_tx = self.w3.eth.account.sign_transaction(transaction_dict=tx, private_key=self.account.key) - - # Send the transaction - tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) - print(f"pool approve weth tx hash: {tx_hash}") - - # deposit tokens into the pool - tx = pool._pool_contract.functions.supply( - self.weth_contract.address, - self.w3.to_wei(10000, "ether"), - self.account.address, - 0, - ).build_transaction( - { - "from": self.w3.to_checksum_address(self.account.address), - "gas": 1000000, - "gasPrice": self.w3.eth.gas_price, - "nonce": self.w3.eth.get_transaction_count(self.account.address), - } - ) - - signed_tx = self.w3.eth.account.sign_transaction(transaction_dict=tx, private_key=self.account.key) - - # Send the transaction - tx_hash = retry_with_backoff(self.w3.eth.send_raw_transaction, signed_tx.rawTransaction) - print(f"supply weth tx hash: {tx_hash}") - reserve_data = retry_with_backoff(pool._pool_contract.functions.getReserveData(pool._underlying_asset_address).call) apy_before = Web3.to_wei(reserve_data.currentLiquidityRate / 1e27, "ether") print(f"apy before rebalancing ether: {apy_before}") - # calculate predicted future supply rate after removing 1000 ETH to end up with 9000 ETH in the pool + # calculate predicted future supply rate after removing 100000 DAI to end up with 9000 DAI in the pool pool.sync(self.w3) apy_after = pool.supply_rate(int(9000e18)) print(f"apy after rebalancing ether: {apy_after}") From ef825b766acc4453e3bff5087967e2cc5e1af141 Mon Sep 17 00:00:00 2001 From: Syeam Bin Abdullah Date: Mon, 18 Nov 2024 00:40:13 +0000 Subject: [PATCH 35/51] fix: zero div. error --- sturdy/validator/reward.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index b4723a6..cbdb81c 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -265,6 +265,9 @@ def annualized_yield_pct( Calculates annualized yields of allocations in pools within scoring period """ + if seconds_passed < 1: + return 0 + # calculate projected yield initial_balance = cast(int, assets_and_pools["total_assets"]) pools = cast(dict[str, ChainBasedPoolModel], assets_and_pools["pools"]) @@ -281,7 +284,9 @@ def annualized_yield_pct( curr_share_price = pool._share_price pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) deposit_delta = allocation - pool._user_deposits - adjusted_pct_delta = (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta) * pct_delta + adjusted_pct_delta = ( + (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta + 1) * pct_delta + ) annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 total_yield += int(allocation * annualized_pct_yield) case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): From 5d01677e183c9bc63aca270224665d1177101184 Mon Sep 17 00:00:00 2001 From: Rubin <157901773+rbnlb@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:27:23 +0100 Subject: [PATCH 36/51] feat: Compute alloc, apy, time based penalties --- sturdy/validator/reward.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index cbdb81c..de900fd 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -25,7 +25,7 @@ import numpy.typing as npt import torch -from sturdy.constants import QUERY_TIMEOUT, SIMILARITY_THRESHOLD +from sturdy.constants import ALLOCATION_SIMILARITY_THRESHOLD, APY_SIMILARITY_THRESHOLD, QUERY_TIMEOUT from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, PoolFactory, check_allocations from sturdy.protocol import AllocationsDict, AllocInfo from sturdy.utils.ethmath import wei_div @@ -99,16 +99,26 @@ def normalize_squared(apys_and_allocations: AllocationsDict, epsilon: float = 1e def calculate_penalties( - similarity_matrix: dict[str, dict[str, float]], + allocation_similarity_matrix: dict[str, dict[str, float]], + apy_similarity_matrix: dict[str, dict[str, float]], axon_times: dict[str, float], - similarity_threshold: float = SIMILARITY_THRESHOLD, + allocation_similarity_threshold: float = ALLOCATION_SIMILARITY_THRESHOLD, + apy_similarity_threshold: float = APY_SIMILARITY_THRESHOLD, ) -> dict[str, int]: - penalties = {miner: 0 for miner in similarity_matrix} - - for miner_a, similarities in similarity_matrix.items(): - for miner_b, similarity in similarities.items(): - if similarity <= similarity_threshold and axon_times[miner_a] <= axon_times[miner_b]: - penalties[miner_b] += 1 + penalties = {miner: 0 for miner in allocation_similarity_matrix} + + for miner_a in allocation_similarity_matrix: + allocation_similarities = allocation_similarity_matrix[miner_a] + apy_similarities = apy_similarity_matrix[miner_a] + for miner_b in allocation_similarities: + allocation_similarity = allocation_similarities[miner_b] + apy_similarity = apy_similarities[miner_b] + if ( + allocation_similarity <= allocation_similarity_threshold + and apy_similarity <= apy_similarity_threshold + and axon_times[miner_a] <= axon_times[miner_b] + ): + penalties[miner_b] += 1 return penalties @@ -193,7 +203,7 @@ def adjust_rewards_for_plagiarism( assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], uids: list, axon_times: dict[str, float], - similarity_threshold: float = SIMILARITY_THRESHOLD, + similarity_threshold: float = ALLOCATION_SIMILARITY_THRESHOLD, ) -> torch.Tensor: """ Adjusts the annual percentage yield (APY) rewards for miners based on the similarity of their allocations From 00863610a9aa3192e8921b9dd9ec252d339b2ce0 Mon Sep 17 00:00:00 2001 From: Rubin <157901773+rbnlb@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:53:33 +0100 Subject: [PATCH 37/51] feat: Adjust reward based on alloc, apy, time --- sturdy/validator/reward.py | 69 +++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index de900fd..1274328 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -114,11 +114,11 @@ def calculate_penalties( allocation_similarity = allocation_similarities[miner_b] apy_similarity = apy_similarities[miner_b] if ( - allocation_similarity <= allocation_similarity_threshold - and apy_similarity <= apy_similarity_threshold - and axon_times[miner_a] <= axon_times[miner_b] - ): - penalties[miner_b] += 1 + allocation_similarity <= allocation_similarity_threshold + and apy_similarity <= apy_similarity_threshold + and axon_times[miner_a] <= axon_times[miner_b] + ): + penalties[miner_b] += 1 return penalties @@ -146,7 +146,7 @@ def get_distance(alloc_a: npt.NDArray, alloc_b: npt.NDArray, total_assets: int) return norm / gmpy2.sqrt(float(2 * total_assets**2)) -def get_similarity_matrix( +def get_allocation_similarity_matrix( apys_and_allocations: dict[str, dict[str, AllocationsDict | int]], assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], ) -> dict[str, dict[str, float]]: @@ -196,6 +196,49 @@ def get_similarity_matrix( return similarity_matrix +def get_apy_similarity_matrix( + apys_and_allocations: dict[str, dict[str, AllocationsDict | int]], +) -> dict[str, dict[str, float]]: + """ + Calculates the similarity matrix for the allocation strategies of miners' APY using normalized Euclidean distance. + + This function computes a similarity matrix based on the Euclidean distance between the allocation vectors of miners, + normalized by the maximum possible distance in the given asset space. Each miner's allocation is compared with every + other miner's allocation, resulting in a matrix where each element (i, j) represents the normalized Euclidean distance + between the allocations of miner_i and miner_j. + + The similarity metric is scaled between 0 and 1, where 0 indicates identical allocations and 1 indicates the maximum + possible distance between the allocation 'vectors'. + + Args: + apys_and_allocations (dict[str, dict[str, Union[AllocationsDict, int]]]): + A dictionary containing the APY and allocation strategies for each miner. The keys are miner identifiers, + and the values are dictionaries with their respective allocations and APYs. + + Returns: + dict[str, dict[str, float]]: + A nested dictionary where each key is a miner identifier, and the value is another dictionary containing the + normalized Euclidean distances to every other miner. The distances are scaled between 0 and 1. + """ + + similarity_matrix = {} + + for miner_a, info_a in apys_and_allocations.items(): + apy_a = cast(int, info_a["apy"]) + apy_a = np.array([gmpy2.mpz(apy_a)]) + similarity_matrix[miner_a] = {} + for miner_b, info_b in apys_and_allocations.items(): + if miner_a != miner_b: + apy_b = cast(int, info_b["apy"]) + if apy_a is None or apy_b is None: + similarity_matrix[miner_a][miner_b] = float("inf") + continue + apy_b = np.array([gmpy2.mpz(apy_b)]) + similarity_matrix[miner_a][miner_b] = get_distance(apy_a, apy_b, max(apy_b, apy_b)[0]) # Max scaling + + return similarity_matrix + + def adjust_rewards_for_plagiarism( self, rewards_apy: torch.Tensor, @@ -203,7 +246,8 @@ def adjust_rewards_for_plagiarism( assets_and_pools: dict[str, dict[str, ChainBasedPoolModel] | int], uids: list, axon_times: dict[str, float], - similarity_threshold: float = ALLOCATION_SIMILARITY_THRESHOLD, + allocation_similarity_threshold: float = ALLOCATION_SIMILARITY_THRESHOLD, + apy_similarity_threshold: float = APY_SIMILARITY_THRESHOLD, ) -> torch.Tensor: """ Adjusts the annual percentage yield (APY) rewards for miners based on the similarity of their allocations @@ -235,10 +279,17 @@ def adjust_rewards_for_plagiarism( to a consistent format suitable for comparison. """ # Step 1: Calculate pairwise similarity (e.g., using Euclidean distance) - similarity_matrix = get_similarity_matrix(apys_and_allocations, assets_and_pools) + allocation_similarity_matrix = get_allocation_similarity_matrix(apys_and_allocations, assets_and_pools) + apy_similarity_matrix = get_apy_similarity_matrix(apys_and_allocations) # Step 2: Apply penalties considering axon times - penalties = calculate_penalties(similarity_matrix, axon_times, similarity_threshold) + penalties = calculate_penalties( + allocation_similarity_matrix, + apy_similarity_matrix, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, + ) self.similarity_penalties = penalties # Step 3: Calculate final rewards with adjusted penalties From 4274cd8fa42e3fd31a3a53ef891dd38d1009be24 Mon Sep 17 00:00:00 2001 From: Rubin <157901773+rbnlb@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:08:25 +0100 Subject: [PATCH 38/51] fix: Update apy similarity calculation --- sturdy/validator/reward.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 1274328..3691321 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -225,16 +225,13 @@ def get_apy_similarity_matrix( for miner_a, info_a in apys_and_allocations.items(): apy_a = cast(int, info_a["apy"]) - apy_a = np.array([gmpy2.mpz(apy_a)]) + apy_a = np.array([gmpy2.mpz(apy_a)], dtype=object) similarity_matrix[miner_a] = {} for miner_b, info_b in apys_and_allocations.items(): if miner_a != miner_b: apy_b = cast(int, info_b["apy"]) - if apy_a is None or apy_b is None: - similarity_matrix[miner_a][miner_b] = float("inf") - continue - apy_b = np.array([gmpy2.mpz(apy_b)]) - similarity_matrix[miner_a][miner_b] = get_distance(apy_a, apy_b, max(apy_b, apy_b)[0]) # Max scaling + apy_b = np.array([gmpy2.mpz(apy_b)], dtype=object) + similarity_matrix[miner_a][miner_b] = get_distance(apy_a, apy_b, max(apy_a, apy_b)[0]) # Max scaling return similarity_matrix From 828c7f4f38077bd190c6b904eb25d14b56548589 Mon Sep 17 00:00:00 2001 From: Rubin <157901773+rbnlb@users.noreply.github.com> Date: Sat, 9 Nov 2024 10:54:15 +0100 Subject: [PATCH 39/51] test: Test alloc, apy, time adjusted rewards --- tests/unit/validator/test_reward_helpers.py | 224 ++++++++++++++++---- 1 file changed, 183 insertions(+), 41 deletions(-) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index cdd2a5b..926fecc 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -19,8 +19,9 @@ calculate_penalties, calculate_rewards_with_adjusted_penalties, format_allocations, + get_allocation_similarity_matrix, + get_apy_similarity_matrix, get_distance, - get_similarity_matrix, normalize_squared, ) @@ -524,7 +525,98 @@ def test_format_allocations_empty(self) -> None: self.assertEqual(result, expected_output) - def test_get_similarity_matrix(self) -> None: + def test_get_allocation_similarity_matrix(self) -> None: + apys_and_allocations = { + "miner_1": { + "apy": int(0.05e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, + "miner_2": { + "apy": int(0.04e18), + "allocations": {"pool_1": 40e18, "pool_2": 10e18}, + }, + "miner_3": { + "apy": int(0.06e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, + } + assets_and_pools = { + "pools": { + "pool_1": {"reserve_size": 100e18}, + "pool_2": {"reserve_size": 100e18}, + }, + "total_assets": 10e18, + } + + total_assets = assets_and_pools["total_assets"] + + expected_similarity_matrix = { + "miner_2": { + "miner_1": get_distance(np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), + "miner_3": get_distance(np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), + }, + "miner_1": { + "miner_2": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), total_assets), + "miner_3": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), + }, + "miner_3": { + "miner_1": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), + "miner_2": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), total_assets), + }, + } + + result = get_allocation_similarity_matrix(apys_and_allocations, assets_and_pools) + + for miner_a in expected_similarity_matrix: + for miner_b in expected_similarity_matrix[miner_a]: + self.assertAlmostEqual( + result[miner_a][miner_b], + expected_similarity_matrix[miner_a][miner_b], + places=5, + ) + + def test_get_apy_similarity_matrix(self) -> None: + apys_and_allocations = { + "miner_1": { + "apy": int(0.05e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, + "miner_2": { + "apy": int(0.04e18), + "allocations": {"pool_1": 40e18, "pool_2": 10e18}, + }, + "miner_3": { + "apy": int(0.06e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, + } + + expected_similarity_matrix = { + "miner_1": { + "miner_2": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.05e18)), + "miner_3": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0.06e18)], dtype=object), gmpy2.mpz(0.06e18)), + }, + "miner_2": { + "miner_1": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18)), + "miner_3": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0.06e18)], dtype=object), gmpy2.mpz(0.06e18)), + }, + "miner_3": { + "miner_1": get_distance(np.array([gmpy2.mpz(0.06e18)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.06e18)), + "miner_2": get_distance(np.array([gmpy2.mpz(0.06e18)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.06e18)), + }, + } + + result = get_apy_similarity_matrix(apys_and_allocations) + + for miner_a in expected_similarity_matrix: + for miner_b in expected_similarity_matrix[miner_a]: + self.assertAlmostEqual( + result[miner_a][miner_b], + expected_similarity_matrix[miner_a][miner_b], + places=5, + ) + + def test_get_allocation_similarity_matrix_empty(self) -> None: apys_and_allocations = { "miner_1": { "apy": int(0.05e18), @@ -534,10 +626,7 @@ def test_get_similarity_matrix(self) -> None: "apy": int(0.04e18), "allocations": {"pool_1": 40, "pool_2": 10}, }, - "miner_3": { - "apy": int(0.06e18), - "allocations": {"pool_1": 30, "pool_2": 20}, - }, + "miner_3": {"apy": 0, "allocations": None}, } assets_and_pools = { "pools": { @@ -548,24 +637,20 @@ def test_get_similarity_matrix(self) -> None: } total_assets = assets_and_pools["total_assets"] - normalization_factor = np.sqrt(float(2 * total_assets**2)) # √(2 * total_assets^2) expected_similarity_matrix = { "miner_1": { - "miner_2": np.linalg.norm(np.array([30, 20]) - np.array([40, 10])) / normalization_factor, - "miner_3": np.linalg.norm(np.array([30, 20]) - np.array([30, 20])) / normalization_factor, + "miner_2": get_distance(np.array([gmpy2.mpz(30), gmpy2.mpz(20)], dtype=object), np.array([gmpy2.mpz(40), gmpy2.mpz(10)], dtype=object), total_assets), + "miner_3": float("inf"), }, "miner_2": { - "miner_1": np.linalg.norm(np.array([40, 10]) - np.array([30, 20])) / normalization_factor, - "miner_3": np.linalg.norm(np.array([40, 10]) - np.array([30, 20])) / normalization_factor, - }, - "miner_3": { - "miner_1": np.linalg.norm(np.array([30, 20]) - np.array([30, 20])) / normalization_factor, - "miner_2": np.linalg.norm(np.array([30, 20]) - np.array([40, 10])) / normalization_factor, + "miner_1": get_distance(np.array([gmpy2.mpz(40), gmpy2.mpz(10)], dtype=object), np.array([gmpy2.mpz(30), gmpy2.mpz(20)], dtype=object), total_assets), + "miner_3": float("inf"), }, + "miner_3": {"miner_1": float("inf"), "miner_2": float("inf")}, } - result = get_similarity_matrix(apys_and_allocations, assets_and_pools) + result = get_allocation_similarity_matrix(apys_and_allocations, assets_and_pools) for miner_a in expected_similarity_matrix: for miner_b in expected_similarity_matrix[miner_a]: @@ -575,7 +660,7 @@ def test_get_similarity_matrix(self) -> None: places=5, ) - def test_get_similarity_matrix_empty(self) -> None: + def test_get_apy_similarity_matrix_empty(self) -> None: apys_and_allocations = { "miner_1": { "apy": int(0.05e18), @@ -596,21 +681,23 @@ def test_get_similarity_matrix_empty(self) -> None: } total_assets = assets_and_pools["total_assets"] - normalization_factor = np.sqrt(float(2 * total_assets**2)) # √(2 * total_assets^2) - expected_similarity_matrix = { + expected_similarity_matrix = { "miner_1": { - "miner_2": np.linalg.norm(np.array([30, 20]) - np.array([40, 10])) / normalization_factor, - "miner_3": float("inf"), + "miner_2": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.05e18)), + "miner_3": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0)], dtype=object), gmpy2.mpz(0.05e18)), }, "miner_2": { - "miner_1": np.linalg.norm(np.array([40, 10]) - np.array([30, 20])) / normalization_factor, - "miner_3": float("inf"), + "miner_1": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18)), + "miner_3": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0)], dtype=object), gmpy2.mpz(0.04e18)), + }, + "miner_3": { + "miner_1": get_distance(np.array([gmpy2.mpz(0)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18)), + "miner_2": get_distance(np.array([gmpy2.mpz(0)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.04e18)), }, - "miner_3": {"miner_1": float("inf"), "miner_2": float("inf")}, } - result = get_similarity_matrix(apys_and_allocations, assets_and_pools) + result = get_apy_similarity_matrix(apys_and_allocations) for miner_a in expected_similarity_matrix: for miner_b in expected_similarity_matrix[miner_a]: @@ -621,44 +708,90 @@ def test_get_similarity_matrix_empty(self) -> None: ) def test_calculate_penalties(self) -> None: - similarity_matrix = { + allocation_similarity_matrix = { + "1": {"2": 0.05, "3": 0.2}, + "2": {"1": 0.05, "3": 0.1}, + "3": {"1": 0.2, "2": 0.1}, + } + apy_similarity_matrix = { "1": {"2": 0.05, "3": 0.2}, "2": {"1": 0.05, "3": 0.1}, "3": {"1": 0.2, "2": 0.1}, } axon_times = {"1": 1.0, "2": 2.0, "3": 3.0} - similarity_threshold = 0.1 + + allocation_similarity_threshold = 0.2 + apy_similarity_threshold = 0.1 expected_penalties = {"1": 0, "2": 1, "3": 1} - result = calculate_penalties(similarity_matrix, axon_times, similarity_threshold) + result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) + + self.assertEqual(result, expected_penalties) + + def test_calculate_penalties_no_apy_similarities(self) -> None: + allocation_similarity_matrix = { + "1": {"2": 0.05, "3": 0.2}, + "2": {"1": 0.05, "3": 0.1}, + "3": {"1": 0.2, "2": 0.1}, + } + apy_similarity_matrix = { + "1": {"2": 0.05, "3": 0.2}, + "2": {"1": 0.05, "3": 0.1}, + "3": {"1": 0.2, "2": 0.1}, + } + axon_times = {"1": 1.0, "2": 2.0, "3": 3.0} + allocation_similarity_threshold = 0.2 + apy_similarity_threshold = 0.05 + + + expected_penalties = {"1": 0, "2": 1, "3": 0} + result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) self.assertEqual(result, expected_penalties) + def test_calculate_penalties_no_similarities(self) -> None: - similarity_matrix = { + allocation_similarity_matrix = { + "1": {"2": 0.5, "3": 0.6}, + "2": {"1": 0.5, "3": 0.7}, + "3": {"1": 0.6, "2": 0.7}, + } + apy_similarity_matrix = { "1": {"2": 0.5, "3": 0.6}, "2": {"1": 0.5, "3": 0.7}, "3": {"1": 0.6, "2": 0.7}, } axon_times = {"1": 1.0, "2": 2.0, "3": 3.0} - similarity_threshold = 0.1 + + allocation_similarity_threshold = 0.3 + apy_similarity_threshold = 0.1 expected_penalties = {"1": 0, "2": 0, "3": 0} - result = calculate_penalties(similarity_matrix, axon_times, similarity_threshold) + result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) self.assertEqual(result, expected_penalties) def test_calculate_penalties_equal_times(self) -> None: - similarity_matrix = { + allocation_similarity_matrix = { "1": {"2": 0.05, "3": 0.05}, "2": {"1": 0.05, "3": 0.05}, "3": {"1": 0.05, "2": 0.05}, } + apy_similarity_matrix = { + "1": {"2": 0.05, "3": 0.05}, + "2": {"1": 0.05, "3": 0.05}, + "3": {"1": 0.05, "2": 0.05}, + } + axon_times = {"1": 1.0, "2": 1.0, "3": 1.0} - similarity_threshold = 0.1 + + allocation_similarity_threshold = 0.1 + + apy_similarity_threshold = 0.2 expected_penalties = {"1": 2, "2": 2, "3": 2} - result = calculate_penalties(similarity_matrix, axon_times, similarity_threshold) + + result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) self.assertEqual(result, expected_penalties) @@ -685,9 +818,9 @@ def test_calculate_rewards_with_no_penalties(self) -> None: def test_adjust_rewards_for_plagiarism(self) -> None: rewards_apy = torch.Tensor([0.05 / 0.05, 0.04 / 0.05, 0.03 / 0.05]) apys_and_allocations = { - "0": {"apy": 0.05, "allocations": {"asset_1": 200, "asset_2": 300}}, - "1": {"apy": 0.04, "allocations": {"asset_1": 202, "asset_2": 303}}, - "2": {"apy": 0.03, "allocations": {"asset_1": 200, "asset_2": 400}}, + "0": {"apy":50, "allocations": {"asset_1": 200, "asset_2": 300}}, # APY: int + "1": {"apy": 40, "allocations": {"asset_1": 202, "asset_2": 303}}, + "2": {"apy": 30, "allocations": {"asset_1": 200, "asset_2": 400}}, } assets_and_pools = { "total_assets": 500, @@ -696,9 +829,14 @@ def test_adjust_rewards_for_plagiarism(self) -> None: uids = ["0", "1", "2"] axon_times = {"0": 1.0, "1": 2.0, "2": 3.0} + allocation_similarity_threshold = .1 + + apy_similarity_threshold = 0.2 + expected_rewards = torch.Tensor([1.0, 0.0, 0.03 / 0.05]) + result = adjust_rewards_for_plagiarism( - self.vali, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times + self.vali, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times, allocation_similarity_threshold, apy_similarity_threshold ) torch.testing.assert_close(result, expected_rewards, rtol=0, atol=1e-5) @@ -706,8 +844,8 @@ def test_adjust_rewards_for_plagiarism(self) -> None: def test_adjust_rewards_for_one_plagiarism(self) -> None: rewards_apy = torch.Tensor([1.0, 1.0]) apys_and_allocations = { - "0": {"apy": 0.05, "allocations": {"asset_1": 200, "asset_2": 300}}, - "1": {"apy": 0.05, "allocations": {"asset_1": 200, "asset_2": 300}}, + "0": {"apy": 50, "allocations": {"asset_1": 200, "asset_2": 300}}, + "1": {"apy": 50, "allocations": {"asset_1": 200, "asset_2": 300}}, } assets_and_pools = { "total_assets": 500, @@ -717,8 +855,12 @@ def test_adjust_rewards_for_one_plagiarism(self) -> None: axon_times = {"0": 1.0, "1": 2.0} expected_rewards = torch.Tensor([1.0, 0.0]) + + allocation_similarity_threshold = .1 + apy_similarity_threshold = 0.2 + result = adjust_rewards_for_plagiarism( - self.vali, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times + self.vali, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times, allocation_similarity_threshold, apy_similarity_threshold ) torch.testing.assert_close(result, expected_rewards, rtol=0, atol=1e-5) From c9156db06682de9cdd566ed43f46668e5f9fbe5a Mon Sep 17 00:00:00 2001 From: Rubin <157901773+rbnlb@users.noreply.github.com> Date: Sat, 9 Nov 2024 12:15:47 +0100 Subject: [PATCH 40/51] feat: Add apy similarity threshold --- sturdy/constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sturdy/constants.py b/sturdy/constants.py index 1d93993..84452c1 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -39,7 +39,8 @@ SCORING_WINDOW = 300 # scoring window TOTAL_ALLOC_THRESHOLD = 0.98 -SIMILARITY_THRESHOLD = 0.01 # similarity threshold for plagiarism checking +ALLOCATION_SIMILARITY_THRESHOLD = 1e-18 # similarity threshold for plagiarism checking +APY_SIMILARITY_THRESHOLD = 1e-16 DB_DIR = "validator_database.db" # default validator database dir From 7ecdbdd96c309e25f0cac52d220a0963e303aa25 Mon Sep 17 00:00:00 2001 From: Rubin <157901773+rbnlb@users.noreply.github.com> Date: Mon, 18 Nov 2024 07:11:27 +0100 Subject: [PATCH 41/51] feat: Increase alloc similarity threshold to 1e-16 --- sturdy/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sturdy/constants.py b/sturdy/constants.py index 84452c1..9d91826 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -39,7 +39,7 @@ SCORING_WINDOW = 300 # scoring window TOTAL_ALLOC_THRESHOLD = 0.98 -ALLOCATION_SIMILARITY_THRESHOLD = 1e-18 # similarity threshold for plagiarism checking +ALLOCATION_SIMILARITY_THRESHOLD = 1e-16 # similarity threshold for plagiarism checking APY_SIMILARITY_THRESHOLD = 1e-16 DB_DIR = "validator_database.db" # default validator database dir From 36809c0a288994fd74ec5a634ed38aa0c3672ffb Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Mon, 18 Nov 2024 11:40:00 +0000 Subject: [PATCH 42/51] chore: increase similarity thresholds + increase norm exp + cleanup --- sturdy/constants.py | 4 +- sturdy/validator/reward.py | 10 +- tests/unit/validator/test_reward_helpers.py | 252 +++++++++++++------- 3 files changed, 177 insertions(+), 89 deletions(-) diff --git a/sturdy/constants.py b/sturdy/constants.py index 9d91826..14a6b17 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -39,8 +39,8 @@ SCORING_WINDOW = 300 # scoring window TOTAL_ALLOC_THRESHOLD = 0.98 -ALLOCATION_SIMILARITY_THRESHOLD = 1e-16 # similarity threshold for plagiarism checking -APY_SIMILARITY_THRESHOLD = 1e-16 +ALLOCATION_SIMILARITY_THRESHOLD = 1e-4 # similarity threshold for plagiarism checking +APY_SIMILARITY_THRESHOLD = 1e-4 DB_DIR = "validator_database.db" # default validator database dir diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 3691321..5b1f5c0 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -84,18 +84,16 @@ def format_allocations( return {contract_addr: allocs[contract_addr] for contract_addr in sorted(allocs.keys())} -def normalize_squared(apys_and_allocations: AllocationsDict, epsilon: float = 1e-8) -> torch.Tensor: +def normalize_exp(apys_and_allocations: AllocationsDict, epsilon: float = 1e-8) -> torch.Tensor: raw_apys = {uid: apys_and_allocations[uid]["apy"] for uid in apys_and_allocations} - # TODO: is there a better way to go about this? if len(raw_apys) <= 1: return torch.zeros(len(raw_apys)) apys = torch.tensor(list(raw_apys.values()), dtype=torch.float32) + normed = (apys - apys.min()) / (apys.max() - apys.min() + epsilon) - squared = torch.pow(apys, 2) - - return (squared - squared.min()) / (squared.max() - squared.min() + epsilon) + return torch.pow(normed, 8) def calculate_penalties( @@ -308,7 +306,7 @@ def _get_rewards( - adjusted_rewards: The reward values for the miners. """ - rewards_apy = normalize_squared(apys_and_allocations).to(self.device) + rewards_apy = normalize_exp(apys_and_allocations).to(self.device) return adjust_rewards_for_plagiarism(self, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times) diff --git a/tests/unit/validator/test_reward_helpers.py b/tests/unit/validator/test_reward_helpers.py index 926fecc..cc2ffd0 100644 --- a/tests/unit/validator/test_reward_helpers.py +++ b/tests/unit/validator/test_reward_helpers.py @@ -22,7 +22,7 @@ get_allocation_similarity_matrix, get_apy_similarity_matrix, get_distance, - normalize_squared, + normalize_exp, ) load_dotenv() @@ -90,7 +90,7 @@ class TestDynamicNormalizeZScore(unittest.TestCase): def test_basic_normalization(self) -> None: # Test a simple AllocationsDict with large values apys_and_allocations = {"1": {"apy": 1e16}, "2": {"apy": 2e16}, "3": {"apy": 3e16}, "4": {"apy": 4e16}} - normalized = normalize_squared(apys_and_allocations) + normalized = normalize_exp(apys_and_allocations) # Check if output is normalized between 0 and 1 self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -105,7 +105,7 @@ def test_with_low_outliers(self) -> None: "4": {"apy": 5e16}, "5": {"apy": 1e17}, } - normalized = normalize_squared(apys_and_allocations) + normalized = normalize_exp(apys_and_allocations) # Check that outliers don't affect the overall normalization self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -120,7 +120,7 @@ def test_with_high_outliers(self) -> None: "4": {"apy": 1e17}, "5": {"apy": 2e17}, } - normalized = normalize_squared(apys_and_allocations) + normalized = normalize_exp(apys_and_allocations) # Check that the function correctly handles high outliers self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -129,7 +129,7 @@ def test_with_high_outliers(self) -> None: def test_uniform_values(self) -> None: # Test where all values are the same apys_and_allocations = {"1": {"apy": 1e16}, "2": {"apy": 1e16}, "3": {"apy": 1e16}, "4": {"apy": 1e16}} - normalized = normalize_squared(apys_and_allocations) + normalized = normalize_exp(apys_and_allocations) # If all values are the same, the output should also be uniform (or handle gracefully) self.assertTrue( @@ -147,7 +147,7 @@ def test_low_variance(self) -> None: "4": {"apy": 1.03e16}, "5": {"apy": 1.04e16}, } - normalized = normalize_squared(apys_and_allocations) + normalized = normalize_exp(apys_and_allocations) # Check if normalization happens correctly self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) @@ -156,30 +156,12 @@ def test_low_variance(self) -> None: def test_high_variance(self) -> None: # Test with high variance data apys_and_allocations = {"1": {"apy": 1e16}, "2": {"apy": 1e17}, "3": {"apy": 5e17}, "4": {"apy": 1e18}} - normalized = normalize_squared(apys_and_allocations) + normalized = normalize_exp(apys_and_allocations) # Ensure that the normalization works even with high variance self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) self.assertAlmostEqual(normalized.max().item(), 1.0, places=5) - def test_quantile_logic(self) -> None: - # Test a case where the lower quartile range affects the lower bound decision - apys_and_allocations = { - "1": {"apy": 1e16}, - "2": {"apy": 2e16}, - "3": {"apy": 3e16}, - "4": {"apy": 4e16}, - "5": {"apy": 1e17}, - "6": {"apy": 2e17}, - "7": {"apy": 3e17}, - "8": {"apy": 4e17}, - } - normalized = normalize_squared(apys_and_allocations) - - # Ensure that quantile-based clipping works as expected - self.assertAlmostEqual(normalized.min().item(), 0.0, places=5) - self.assertAlmostEqual(normalized.max().item(), 1.0, places=5) - class TestRewardFunctions(unittest.TestCase): @classmethod @@ -527,18 +509,18 @@ def test_format_allocations_empty(self) -> None: def test_get_allocation_similarity_matrix(self) -> None: apys_and_allocations = { - "miner_1": { - "apy": int(0.05e18), - "allocations": {"pool_1": 30e18, "pool_2": 20e18}, - }, - "miner_2": { - "apy": int(0.04e18), - "allocations": {"pool_1": 40e18, "pool_2": 10e18}, - }, - "miner_3": { - "apy": int(0.06e18), - "allocations": {"pool_1": 30e18, "pool_2": 20e18}, - }, + "miner_1": { + "apy": int(0.05e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, + "miner_2": { + "apy": int(0.04e18), + "allocations": {"pool_1": 40e18, "pool_2": 10e18}, + }, + "miner_3": { + "apy": int(0.06e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, } assets_and_pools = { "pools": { @@ -552,16 +534,40 @@ def test_get_allocation_similarity_matrix(self) -> None: expected_similarity_matrix = { "miner_2": { - "miner_1": get_distance(np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), - "miner_3": get_distance(np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), + "miner_1": get_distance( + np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + total_assets, + ), + "miner_3": get_distance( + np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + total_assets, + ), }, "miner_1": { - "miner_2": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), total_assets), - "miner_3": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), + "miner_2": get_distance( + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), + total_assets, + ), + "miner_3": get_distance( + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + total_assets, + ), }, "miner_3": { - "miner_1": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), total_assets), - "miner_2": get_distance(np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), total_assets), + "miner_1": get_distance( + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + total_assets, + ), + "miner_2": get_distance( + np.array([gmpy2.mpz(30e18), gmpy2.mpz(20e18)], dtype=object), + np.array([gmpy2.mpz(40e18), gmpy2.mpz(10e18)], dtype=object), + total_assets, + ), }, } @@ -577,32 +583,56 @@ def test_get_allocation_similarity_matrix(self) -> None: def test_get_apy_similarity_matrix(self) -> None: apys_and_allocations = { - "miner_1": { - "apy": int(0.05e18), - "allocations": {"pool_1": 30e18, "pool_2": 20e18}, - }, - "miner_2": { - "apy": int(0.04e18), - "allocations": {"pool_1": 40e18, "pool_2": 10e18}, - }, - "miner_3": { - "apy": int(0.06e18), - "allocations": {"pool_1": 30e18, "pool_2": 20e18}, - }, + "miner_1": { + "apy": int(0.05e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, + "miner_2": { + "apy": int(0.04e18), + "allocations": {"pool_1": 40e18, "pool_2": 10e18}, + }, + "miner_3": { + "apy": int(0.06e18), + "allocations": {"pool_1": 30e18, "pool_2": 20e18}, + }, } expected_similarity_matrix = { "miner_1": { - "miner_2": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.05e18)), - "miner_3": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0.06e18)], dtype=object), gmpy2.mpz(0.06e18)), + "miner_2": get_distance( + np.array([gmpy2.mpz(0.05e18)], dtype=object), + np.array([gmpy2.mpz(0.04e18)], dtype=object), + gmpy2.mpz(0.05e18), + ), + "miner_3": get_distance( + np.array([gmpy2.mpz(0.05e18)], dtype=object), + np.array([gmpy2.mpz(0.06e18)], dtype=object), + gmpy2.mpz(0.06e18), + ), }, "miner_2": { - "miner_1": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18)), - "miner_3": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0.06e18)], dtype=object), gmpy2.mpz(0.06e18)), + "miner_1": get_distance( + np.array([gmpy2.mpz(0.04e18)], dtype=object), + np.array([gmpy2.mpz(0.05e18)], dtype=object), + gmpy2.mpz(0.05e18), + ), + "miner_3": get_distance( + np.array([gmpy2.mpz(0.04e18)], dtype=object), + np.array([gmpy2.mpz(0.06e18)], dtype=object), + gmpy2.mpz(0.06e18), + ), }, "miner_3": { - "miner_1": get_distance(np.array([gmpy2.mpz(0.06e18)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.06e18)), - "miner_2": get_distance(np.array([gmpy2.mpz(0.06e18)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.06e18)), + "miner_1": get_distance( + np.array([gmpy2.mpz(0.06e18)], dtype=object), + np.array([gmpy2.mpz(0.05e18)], dtype=object), + gmpy2.mpz(0.06e18), + ), + "miner_2": get_distance( + np.array([gmpy2.mpz(0.06e18)], dtype=object), + np.array([gmpy2.mpz(0.04e18)], dtype=object), + gmpy2.mpz(0.06e18), + ), }, } @@ -640,11 +670,19 @@ def test_get_allocation_similarity_matrix_empty(self) -> None: expected_similarity_matrix = { "miner_1": { - "miner_2": get_distance(np.array([gmpy2.mpz(30), gmpy2.mpz(20)], dtype=object), np.array([gmpy2.mpz(40), gmpy2.mpz(10)], dtype=object), total_assets), + "miner_2": get_distance( + np.array([gmpy2.mpz(30), gmpy2.mpz(20)], dtype=object), + np.array([gmpy2.mpz(40), gmpy2.mpz(10)], dtype=object), + total_assets, + ), "miner_3": float("inf"), }, "miner_2": { - "miner_1": get_distance(np.array([gmpy2.mpz(40), gmpy2.mpz(10)], dtype=object), np.array([gmpy2.mpz(30), gmpy2.mpz(20)], dtype=object), total_assets), + "miner_1": get_distance( + np.array([gmpy2.mpz(40), gmpy2.mpz(10)], dtype=object), + np.array([gmpy2.mpz(30), gmpy2.mpz(20)], dtype=object), + total_assets, + ), "miner_3": float("inf"), }, "miner_3": {"miner_1": float("inf"), "miner_2": float("inf")}, @@ -682,18 +720,34 @@ def test_get_apy_similarity_matrix_empty(self) -> None: total_assets = assets_and_pools["total_assets"] - expected_similarity_matrix = { + expected_similarity_matrix = { "miner_1": { - "miner_2": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.05e18)), - "miner_3": get_distance(np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0)], dtype=object), gmpy2.mpz(0.05e18)), + "miner_2": get_distance( + np.array([gmpy2.mpz(0.05e18)], dtype=object), + np.array([gmpy2.mpz(0.04e18)], dtype=object), + gmpy2.mpz(0.05e18), + ), + "miner_3": get_distance( + np.array([gmpy2.mpz(0.05e18)], dtype=object), np.array([gmpy2.mpz(0)], dtype=object), gmpy2.mpz(0.05e18) + ), }, "miner_2": { - "miner_1": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18)), - "miner_3": get_distance(np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0)], dtype=object), gmpy2.mpz(0.04e18)), + "miner_1": get_distance( + np.array([gmpy2.mpz(0.04e18)], dtype=object), + np.array([gmpy2.mpz(0.05e18)], dtype=object), + gmpy2.mpz(0.05e18), + ), + "miner_3": get_distance( + np.array([gmpy2.mpz(0.04e18)], dtype=object), np.array([gmpy2.mpz(0)], dtype=object), gmpy2.mpz(0.04e18) + ), }, "miner_3": { - "miner_1": get_distance(np.array([gmpy2.mpz(0)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18)), - "miner_2": get_distance(np.array([gmpy2.mpz(0)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.04e18)), + "miner_1": get_distance( + np.array([gmpy2.mpz(0)], dtype=object), np.array([gmpy2.mpz(0.05e18)], dtype=object), gmpy2.mpz(0.05e18) + ), + "miner_2": get_distance( + np.array([gmpy2.mpz(0)], dtype=object), np.array([gmpy2.mpz(0.04e18)], dtype=object), gmpy2.mpz(0.04e18) + ), }, } @@ -724,7 +778,13 @@ def test_calculate_penalties(self) -> None: apy_similarity_threshold = 0.1 expected_penalties = {"1": 0, "2": 1, "3": 1} - result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) + result = calculate_penalties( + allocation_similarity_matrix, + apy_similarity_matrix, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, + ) self.assertEqual(result, expected_penalties) @@ -743,13 +803,17 @@ def test_calculate_penalties_no_apy_similarities(self) -> None: allocation_similarity_threshold = 0.2 apy_similarity_threshold = 0.05 - expected_penalties = {"1": 0, "2": 1, "3": 0} - result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) + result = calculate_penalties( + allocation_similarity_matrix, + apy_similarity_matrix, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, + ) self.assertEqual(result, expected_penalties) - def test_calculate_penalties_no_similarities(self) -> None: allocation_similarity_matrix = { "1": {"2": 0.5, "3": 0.6}, @@ -767,7 +831,13 @@ def test_calculate_penalties_no_similarities(self) -> None: apy_similarity_threshold = 0.1 expected_penalties = {"1": 0, "2": 0, "3": 0} - result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) + result = calculate_penalties( + allocation_similarity_matrix, + apy_similarity_matrix, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, + ) self.assertEqual(result, expected_penalties) @@ -791,7 +861,13 @@ def test_calculate_penalties_equal_times(self) -> None: expected_penalties = {"1": 2, "2": 2, "3": 2} - result = calculate_penalties(allocation_similarity_matrix,apy_similarity_matrix, axon_times, allocation_similarity_threshold, apy_similarity_threshold) + result = calculate_penalties( + allocation_similarity_matrix, + apy_similarity_matrix, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, + ) self.assertEqual(result, expected_penalties) @@ -818,7 +894,7 @@ def test_calculate_rewards_with_no_penalties(self) -> None: def test_adjust_rewards_for_plagiarism(self) -> None: rewards_apy = torch.Tensor([0.05 / 0.05, 0.04 / 0.05, 0.03 / 0.05]) apys_and_allocations = { - "0": {"apy":50, "allocations": {"asset_1": 200, "asset_2": 300}}, # APY: int + "0": {"apy": 50, "allocations": {"asset_1": 200, "asset_2": 300}}, # APY: int "1": {"apy": 40, "allocations": {"asset_1": 202, "asset_2": 303}}, "2": {"apy": 30, "allocations": {"asset_1": 200, "asset_2": 400}}, } @@ -829,14 +905,21 @@ def test_adjust_rewards_for_plagiarism(self) -> None: uids = ["0", "1", "2"] axon_times = {"0": 1.0, "1": 2.0, "2": 3.0} - allocation_similarity_threshold = .1 + allocation_similarity_threshold = 0.1 apy_similarity_threshold = 0.2 expected_rewards = torch.Tensor([1.0, 0.0, 0.03 / 0.05]) result = adjust_rewards_for_plagiarism( - self.vali, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times, allocation_similarity_threshold, apy_similarity_threshold + self.vali, + rewards_apy, + apys_and_allocations, + assets_and_pools, + uids, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, ) torch.testing.assert_close(result, expected_rewards, rtol=0, atol=1e-5) @@ -856,11 +939,18 @@ def test_adjust_rewards_for_one_plagiarism(self) -> None: expected_rewards = torch.Tensor([1.0, 0.0]) - allocation_similarity_threshold = .1 + allocation_similarity_threshold = 0.1 apy_similarity_threshold = 0.2 result = adjust_rewards_for_plagiarism( - self.vali, rewards_apy, apys_and_allocations, assets_and_pools, uids, axon_times, allocation_similarity_threshold, apy_similarity_threshold + self.vali, + rewards_apy, + apys_and_allocations, + assets_and_pools, + uids, + axon_times, + allocation_similarity_threshold, + apy_similarity_threshold, ) torch.testing.assert_close(result, expected_rewards, rtol=0, atol=1e-5) From 021ac6e90ea3a8ac57475812efe0883c5c2f1147 Mon Sep 17 00:00:00 2001 From: Syeam Bin Abdullah Date: Tue, 19 Nov 2024 03:01:02 +0000 Subject: [PATCH 43/51] fix:overflow with small alloc --- sturdy/pools.py | 2 +- sturdy/validator/reward.py | 45 ++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/sturdy/pools.py b/sturdy/pools.py index 38ad2fd..3fe7570 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -33,7 +33,7 @@ from sturdy.constants import * from sturdy.pool_registry.pool_registry import POOL_REGISTRY -from sturdy.utils.ethmath import wei_div, wei_mul +from sturdy.utils.ethmath import wei_div from sturdy.utils.misc import ( getReserveFactor, rayMul, diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 5b1f5c0..8908222 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -336,23 +336,36 @@ def annualized_yield_pct( allocation = allocations[contract_addr] match pool.pool_type: case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): - last_share_price = extra_metadata[contract_addr] - curr_share_price = pool._share_price - pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) - deposit_delta = allocation - pool._user_deposits - adjusted_pct_delta = ( - (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta + 1) * pct_delta - ) - annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 - total_yield += int(allocation * annualized_pct_yield) + # TODO: temp fix + if allocation > 0: + last_share_price = extra_metadata[contract_addr] + curr_share_price = pool._share_price + pct_delta = float(curr_share_price - last_share_price) / float(last_share_price) + deposit_delta = allocation - pool._user_deposits + try: + adjusted_pct_delta = ( + (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta + 1) * pct_delta + ) + annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 + total_yield += int(allocation * annualized_pct_yield) + except Exception as e: + bt.logging.error("Error calculating annualized pct yield, skipping:") + bt.logging.error(e) case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - last_income = extra_metadata[contract_addr] - curr_income = pool._normalized_income - pct_delta = float(curr_income - last_income) / float(last_income) - deposit_delta = allocation - pool._user_deposits - adjusted_pct_delta = (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta) * pct_delta - annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 - total_yield += int(allocation * annualized_pct_yield) + if allocation > 0: + last_income = extra_metadata[contract_addr] + curr_income = pool._normalized_income + pct_delta = float(curr_income - last_income) / float(last_income) + deposit_delta = allocation - pool._user_deposits + try: + adjusted_pct_delta = ( + (pool._total_supplied_assets) / (pool._total_supplied_assets + deposit_delta) * pct_delta + ) + annualized_pct_yield = ((1 + adjusted_pct_delta) ** (seconds_per_year / seconds_passed)) - 1 + total_yield += int(allocation * annualized_pct_yield) + except Exception as e: + bt.logging.error("Error calculating annualized pct yield, skipping:") + bt.logging.error(e) case _: total_yield += 0 From 9d3af5736a803242414d5d40e022e763e95be655 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 04:33:33 +0000 Subject: [PATCH 44/51] feat: yearn pool support + more pools in pool registry --- sturdy/pool_registry/pool_registry.py | 30 +++++++++++++++++++ sturdy/pools.py | 38 ++++++++++++++++++++---- sturdy/validator/forward.py | 2 +- sturdy/validator/reward.py | 2 +- tests/unit/validator/test_pool_models.py | 13 ++++++++ 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py index cdc2781..26bc93f 100644 --- a/sturdy/pool_registry/pool_registry.py +++ b/sturdy/pool_registry/pool_registry.py @@ -37,6 +37,21 @@ } }, }, + "Sturdy GHO Aggregator": { + "user_address": "0x93eBc3cA85F96aFD72edB914e833Fe18888DE179", + "assets_and_pools": { + "pools": { + "0x0b8C80fd9CaC5570Ff829416f0aFCE7aF6F3C6f8": { + "pool_type": "STURDY_SILO", + "contract_address": "0x0b8C80fd9CaC5570Ff829416f0aFCE7aF6F3C6f8", + }, + "0xb3Bf04A939aAcFf5BdCFc273CE4F36CF29F063Db": { + "pool_type": "STURDY_SILO", + "contract_address": "0xb3Bf04A939aAcFf5BdCFc273CE4F36CF29F063Db", + }, + } + }, + }, "Morpho USDC Vaults": { "user_address": "0xFA60E843a52eff94901f43ac08232b59351192cc", "total_assets": 1000000000000, @@ -61,4 +76,19 @@ } }, }, + "Yearn DAI-2 Vaults": { + "user_address": "0x92545bCE636E6eE91D88D2D017182cD0bd2fC22e", + "assets_and_pools": { + "pools": { + "0x028eC7330ff87667b6dfb0D94b954c820195336c": { + "pool_type": "YEARN_V3", + "contract_address": "0x028eC7330ff87667b6dfb0D94b954c820195336c", + }, + "0x6164045FC2b2b269ffcaB2197736A74B1725B6C6": { + "pool_type": "YEARN_V3", + "contract_address": "0x6164045FC2b2b269ffcaB2197736A74B1725B6C6", + }, + } + }, + }, } diff --git a/sturdy/pools.py b/sturdy/pools.py index 3fe7570..4e8dc64 100644 --- a/sturdy/pools.py +++ b/sturdy/pools.py @@ -1124,6 +1124,10 @@ class YearnV3Vault(ChainBasedPoolModel): _apr_oracle: Contract = PrivateAttr() _max_withdraw: int = PrivateAttr() _user_deposits: int = PrivateAttr() + _asset: Contract = PrivateAttr() + _total_supplied_assets: int = PrivateAttr() + _user_asset_balance: int = PrivateAttr() + _share_price: int = PrivateAttr() def pool_init(self, web3_provider: Web3) -> None: vault_abi_file_path = Path(__file__).parent / "abi/Yearn_V3_Vault.json" @@ -1142,6 +1146,15 @@ def pool_init(self, web3_provider: Web3) -> None: apr_oracle = web3_provider.eth.contract(abi=apr_oracle_abi, decode_tuples=True) self._apr_oracle = retry_with_backoff(apr_oracle, address=APR_ORACLE) + erc20_abi_file_path = Path(__file__).parent / "abi/IERC20.json" + erc20_abi_file = erc20_abi_file_path.open() + erc20_abi = json.load(erc20_abi_file) + erc20_abi_file.close() + + asset_address = retry_with_backoff(self._vault_contract.functions.asset().call) + asset_contract = web3_provider.eth.contract(abi=erc20_abi, decode_tuples=True) + self._asset = retry_with_backoff(asset_contract, address=asset_address) + def sync(self, web3_provider: Web3) -> None: if not self._initted: self.pool_init(web3_provider) @@ -1149,6 +1162,11 @@ def sync(self, web3_provider: Web3) -> None: self._max_withdraw = retry_with_backoff(self._vault_contract.functions.maxWithdraw(self.user_address).call) user_shares = retry_with_backoff(self._vault_contract.functions.balanceOf(self.user_address).call) self._user_deposits = retry_with_backoff(self._vault_contract.functions.convertToAssets(user_shares).call) + self._total_supplied_assets: Any = retry_with_backoff(self._vault_contract.functions.totalAssets().call) + self._user_asset_balance = retry_with_backoff(self._asset.functions.balanceOf(self.user_address).call) + + # get current price per share + self._share_price = retry_with_backoff(self._vault_contract.functions.pricePerShare().call) def supply_rate(self, amount: int) -> int: delta = amount - self._user_deposits @@ -1202,7 +1220,13 @@ def assets_pools_for_challenge_data( first_pool = pool_list[0] first_pool.sync(web3_provider) match first_pool.pool_type: - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET, POOL_TYPES.MORPHO): + case T if T in ( + POOL_TYPES.STURDY_SILO, + POOL_TYPES.AAVE_DEFAULT, + POOL_TYPES.AAVE_TARGET, + POOL_TYPES.MORPHO, + POOL_TYPES.YEARN_V3, + ): total_assets = first_pool._user_asset_balance case _: pass @@ -1211,11 +1235,13 @@ def assets_pools_for_challenge_data( pool.sync(web3_provider) total_asset = 0 match pool.pool_type: - case POOL_TYPES.STURDY_SILO: - total_asset += pool._user_deposits - case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - total_asset += pool._user_deposits - case POOL_TYPES.MORPHO: + case T if T in ( + POOL_TYPES.STURDY_SILO, + POOL_TYPES.AAVE_DEFAULT, + POOL_TYPES.AAVE_TARGET, + POOL_TYPES.MORPHO, + POOL_TYPES.YEARN_V3, + ): total_asset += pool._user_deposits case _: pass diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index d53201c..9d86697 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -86,7 +86,7 @@ def get_metadata(pools: dict[str, ChainBasedPoolModel], w3: Web3) -> dict: for contract_addr, pool in pools.items(): pool.sync(w3) match pool.pool_type: - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO, POOL_TYPES.YEARN_V3): metadata[contract_addr] = pool._share_price case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): metadata[contract_addr] = pool._normalized_income diff --git a/sturdy/validator/reward.py b/sturdy/validator/reward.py index 8908222..a1b3bb6 100644 --- a/sturdy/validator/reward.py +++ b/sturdy/validator/reward.py @@ -335,7 +335,7 @@ def annualized_yield_pct( for contract_addr, pool in pools.items(): allocation = allocations[contract_addr] match pool.pool_type: - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO): + case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.MORPHO, POOL_TYPES.YEARN_V3): # TODO: temp fix if allocation > 0: last_share_price = extra_metadata[contract_addr] diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index d7169bf..32a4baf 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -781,6 +781,19 @@ def test_vault_pool_model(self) -> None: self.assertTrue(isinstance(pool._apr_oracle, Contract)) self.assertEqual(pool._apr_oracle.address, APR_ORACLE) + self.assertTrue(hasattr(pool, "_user_deposits")) + self.assertTrue(isinstance(pool._user_deposits, int)) + + self.assertTrue(hasattr(pool, "_user_asset_balance")) + self.assertTrue(isinstance(pool._user_asset_balance, int)) + print(f"user asset balance: {pool._user_asset_balance}") + self.assertGreater(pool._user_asset_balance, 0) + + self.assertTrue(hasattr(pool, "_share_price")) + self.assertTrue(isinstance(pool._share_price, int)) + print(f"morpho vault share price: {pool._share_price}") + self.assertGreater(pool._share_price, 0) + # check pool supply_rate print(pool.supply_rate(0)) From 6013cb0cc8b0fca5cb4ddcc28bb4bb4a7bee11d6 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 04:50:34 +0000 Subject: [PATCH 45/51] feat: add Sturdy tBTC Aggregator --- sturdy/pool_registry/pool_registry.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/sturdy/pool_registry/pool_registry.py b/sturdy/pool_registry/pool_registry.py index 26bc93f..5f84459 100644 --- a/sturdy/pool_registry/pool_registry.py +++ b/sturdy/pool_registry/pool_registry.py @@ -52,6 +52,25 @@ } }, }, + "Sturdy tBTC Aggregator": { + "user_address": "0xAeD098db0e39bed6DDc2c07727B8FfC0BA470D9C", + "assets_and_pools": { + "pools": { + "0x6F03c615a3E609D2CF149754CC55462b6477965c": { + "pool_type": "STURDY_SILO", + "contract_address": "0x6F03c615a3E609D2CF149754CC55462b6477965c", + }, + "0xf94B349d52c542aBd8Fb612c2854974e1D72223B": { + "pool_type": "STURDY_SILO", + "contract_address": "0xf94B349d52c542aBd8Fb612c2854974e1D72223B", + }, + "0xEEF271A0071423EA56d38E4aBE748165cc432e3f": { + "pool_type": "STURDY_SILO", + "contract_address": "0xEEF271A0071423EA56d38E4aBE748165cc432e3f", + }, + } + }, + }, "Morpho USDC Vaults": { "user_address": "0xFA60E843a52eff94901f43ac08232b59351192cc", "total_assets": 1000000000000, From 40e6b85b25369dccc91d95614fa35e661304f6f0 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 05:32:21 +0000 Subject: [PATCH 46/51] chore: cleanup --- sturdy/validator/forward.py | 1 - sturdy/validator/sql.py | 1 - tests/unit/validator/test_pool_models.py | 1 - 3 files changed, 3 deletions(-) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 9d86697..6251725 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -49,7 +49,6 @@ async def forward(self) -> Any: bt.logging.debug(f"Purged {rows_affected} stale active allocation requests") # initialize pools and assets - # TODO: only sturdy silos and morpho vaults for now challenge_data = generate_challenge_data(self.w3) request_uuid = str(uuid.uuid4()).replace("-", "") user_address = challenge_data.get("user_address", None) diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index 001aaf6..e8083b2 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -201,7 +201,6 @@ def get_active_allocs(conn: sqlite3.Connection, scoring_window: float = SCORING_ # TODO: change the logic of handling "active allocations" # for now we simply get ones which are still in their "challenge" # period, and consider them to determine the score of miners - # TODO: the existance "active" column may be redundant query = f""" SELECT * FROM {ACTIVE_ALLOCS} WHERE scoring_period_end >= ? diff --git a/tests/unit/validator/test_pool_models.py b/tests/unit/validator/test_pool_models.py index 32a4baf..c6e7469 100644 --- a/tests/unit/validator/test_pool_models.py +++ b/tests/unit/validator/test_pool_models.py @@ -130,7 +130,6 @@ def test_sync(self) -> None: self.assertGreaterEqual(pool._normalized_income, int(1e27)) print(f"normalized income: {pool._normalized_income}") - # TODO: get snapshots working correctly so we are not under the mercy of the automatic ordering of tests def test_supply_rate_alloc(self) -> None: print("----==== TestAavePool | test_supply_rate_increase_alloc ====----") pool = AaveV3DefaultInterestRateV2Pool( From e8287da80456c3a207bd58fad26e080570e23bc0 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 11:29:32 +0000 Subject: [PATCH 47/51] feat: remove active alloc after scoring them --- sturdy/validator/forward.py | 17 ++++++++++++++++- sturdy/validator/sql.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/sturdy/validator/forward.py b/sturdy/validator/forward.py index 6251725..862fbce 100644 --- a/sturdy/validator/forward.py +++ b/sturdy/validator/forward.py @@ -29,7 +29,13 @@ from sturdy.pools import POOL_TYPES, ChainBasedPoolModel, generate_challenge_data from sturdy.protocol import REQUEST_TYPES, AllocateAssets, AllocInfo from sturdy.validator.reward import filter_allocations, get_rewards -from sturdy.validator.sql import delete_stale_active_allocs, get_active_allocs, get_db_connection, log_allocations +from sturdy.validator.sql import ( + delete_active_allocs, + delete_stale_active_allocs, + get_active_allocs, + get_db_connection, + log_allocations, +) async def forward(self) -> Any: @@ -179,7 +185,10 @@ async def query_and_score_miners( bt.logging.debug(f"Active allocs: {active_alloc_rows}") + uids_to_delete = [] for active_alloc in active_alloc_rows: + request_uid = active_alloc["request_uid"] + uids_to_delete.append(request_uid) # calculate rewards for previous active allocations miner_uids, rewards = get_rewards(self, active_alloc) bt.logging.debug(f"miner rewards: {rewards}") @@ -193,6 +202,12 @@ async def query_and_score_miners( int_miner_uids = [int(uid) for uid in miner_uids] self.update_scores(rewards, int_miner_uids) + # wipe these allocations from the db after scoring them + if len(uids_to_delete) > 0: + with get_db_connection(self.config.db_dir) as conn: + rows_affected = delete_active_allocs(conn, uids_to_delete) + bt.logging.debug(f"Scored and removed {rows_affected} active allocation requests") + # before logging latest allocations # filter them axon_times, filtered_allocs = filter_allocations( diff --git a/sturdy/validator/sql.py b/sturdy/validator/sql.py index e8083b2..4b73173 100644 --- a/sturdy/validator/sql.py +++ b/sturdy/validator/sql.py @@ -232,6 +232,22 @@ def delete_stale_active_allocs(conn: sqlite3.Connection, scoring_window: int = S return cur.rowcount +def delete_active_allocs(conn: sqlite3.Connection, uids_to_delete: list[str]) -> int: + if len(uids_to_delete) < 1 or uids_to_delete is None: + return 0 + + placeholders = ", ".join(["?"] * len(uids_to_delete)) + query = f""" + DELETE FROM {ACTIVE_ALLOCS} + WHERE request_uid in ({placeholders}) + """ + + cur = conn.execute(query, uids_to_delete) + conn.commit() + + return cur.rowcount + + def get_miner_responses( conn: sqlite3.Connection, request_uid: str | None = None, From db1a79649df3dd1974fc387fd2b28e7c79e8b21a Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 12:49:28 +0000 Subject: [PATCH 48/51] chore: update scoring params for mainnet release --- sturdy/constants.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sturdy/constants.py b/sturdy/constants.py index 14a6b17..efc61ed 100644 --- a/sturdy/constants.py +++ b/sturdy/constants.py @@ -28,15 +28,15 @@ STOCHASTICITY_STEP = 0.0001 POOL_RESERVE_SIZE = int(100e18) # 100 -QUERY_RATE = 20 # how often synthetic validator queries miners (blocks) +QUERY_RATE = 50 # how often synthetic validator queries miners (blocks) QUERY_TIMEOUT = 45 # timeout (seconds) -ORGANIC_SCORING_PERIOD = 7200 # organic scoring period in seconds -MIN_SCORING_PERIOD = 5400 # min. synthetic scoring period in seconds -MAX_SCORING_PERIOD = 10800 # max. synthetic scoring period in seconds -SCORING_PERIOD_STEP = 1800 +ORGANIC_SCORING_PERIOD = 28800 # scoring period in seconds +MIN_SCORING_PERIOD = 7200 # scoring period in seconds +MAX_SCORING_PERIOD = 43200 # scoring period in seconds +SCORING_PERIOD_STEP = 3600 # scoring period in seconds -SCORING_WINDOW = 300 # scoring window +SCORING_WINDOW = 420 # scoring window (seconds) TOTAL_ALLOC_THRESHOLD = 0.98 ALLOCATION_SIMILARITY_THRESHOLD = 1e-4 # similarity threshold for plagiarism checking From b63a2f2195c23c86ae850ed4182559dfb87bbd41 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 12:50:56 +0000 Subject: [PATCH 49/51] chore: bump version number to 2.0.0 --- min_compute.yml | 2 +- sturdy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/min_compute.yml b/min_compute.yml index b4051d8..b983ff6 100644 --- a/min_compute.yml +++ b/min_compute.yml @@ -10,7 +10,7 @@ # Even then - storage isn't utilized very often - so disk performance won't really be a bottleneck vs, CPU, RAM, # and network bandwidth. -version: '1.5.3' # update this version key as needed, ideally should match your release version +version: '2.0.0' # update this version key as needed, ideally should match your release version compute_spec: diff --git a/sturdy/__init__.py b/sturdy/__init__.py index 13d695f..e793be8 100644 --- a/sturdy/__init__.py +++ b/sturdy/__init__.py @@ -17,7 +17,7 @@ # DEALINGS IN THE SOFTWARE. # Define the version of the template module. -__version__ = "1.5.3" +__version__ = "2.0.0" version_split = __version__.split(".") __spec_version__ = (1000 * int(version_split[0])) + (10 * int(version_split[1])) + (1 * int(version_split[2])) From 0de545f808545e46758f90fb26d75ce0a00a92c3 Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 13:46:20 +0000 Subject: [PATCH 50/51] fix: add rank to allocinfo --- sturdy/protocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sturdy/protocol.py b/sturdy/protocol.py index deeb6c7..a47a947 100644 --- a/sturdy/protocol.py +++ b/sturdy/protocol.py @@ -36,6 +36,7 @@ class REQUEST_TYPES(IntEnum): class AllocInfo(TypedDict): + rank: int allocations: AllocationsDict | None From 3aafcdacbf493db5dc94a80c23fc98fe886eae3c Mon Sep 17 00:00:00 2001 From: shr1ftyy Date: Tue, 19 Nov 2024 14:24:13 +0000 Subject: [PATCH 51/51] chore: update docs --- README.md | 21 +++++++++++++++------ docs/validator.md | 19 +++++++++---------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6258da7..1e803ed 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,13 @@ There are three core files. first_pool = pool_list[0] first_pool.sync(web3_provider) match first_pool.pool_type: - case T if T in (POOL_TYPES.STURDY_SILO, POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET, POOL_TYPES.MORPHO): + case T if T in ( + POOL_TYPES.STURDY_SILO, + POOL_TYPES.AAVE_DEFAULT, + POOL_TYPES.AAVE_TARGET, + POOL_TYPES.MORPHO, + POOL_TYPES.YEARN_V3, + ): total_assets = first_pool._user_asset_balance case _: pass @@ -103,11 +109,13 @@ There are three core files. pool.sync(web3_provider) total_asset = 0 match pool.pool_type: - case POOL_TYPES.STURDY_SILO: - total_asset += pool._user_deposits - case T if T in (POOL_TYPES.AAVE_DEFAULT, POOL_TYPES.AAVE_TARGET): - total_asset += pool._user_deposits - case POOL_TYPES.MORPHO: + case T if T in ( + POOL_TYPES.STURDY_SILO, + POOL_TYPES.AAVE_DEFAULT, + POOL_TYPES.AAVE_TARGET, + POOL_TYPES.MORPHO, + POOL_TYPES.YEARN_V3, + ): total_asset += pool._user_deposits case _: pass @@ -121,6 +129,7 @@ There are three core files. challenge_data["user_address"] = global_user_address return challenge_data + ``` Validators can optionally run an API server and sell their bandwidth to outside users to send their own pools (organic requests) to the subnet. For more information on this process - please read diff --git a/docs/validator.md b/docs/validator.md index b9e4564..6357efa 100644 --- a/docs/validator.md +++ b/docs/validator.md @@ -27,6 +27,14 @@ You will need `pm2` if you would like to utilize the auto update scripts that co 1. Install [node and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 2. Install [pm2](https://pm2.io) +### Creating the database +Used to store api keys (only for organic validators), scoring logs, and "active" miner allocations for scoring + +First, [install dbmate](https://github.com/amacneil/dbmate?tab=readme-ov-file#installation). then run the command below +```bash +dbmate --url "sqlite:validator_database.db" up +``` + ## Running a Validator @@ -140,15 +148,6 @@ Where `ID_OR_PROCESS_NAME` is the `name` OR `id` of the process as noted per the ## Selling your bandwidth -### Creating the database -Used to store api keys & scoring logs - -First, [install dbmate](https://github.com/amacneil/dbmate?tab=readme-ov-file#installation) - -```bash -dbmate --url "sqlite:validator_database.db" up -``` - ### Managing access To manage access to the your api server and sell access to anyone you like, using the sturdy-cli is the easiest way. @@ -172,7 +171,7 @@ To get more info about that command! For example: ```bash -sturdy create-key 10 60 test +sturdy create-key --balance 10 --rate-limit-per-minute 60 --name test ``` Creates a test key with a balance of 10 (which corresponds to 10 requests), a rate limit of 60 requests per minute = 1/s, and a name 'test'.