# Week 18-19 development update I put together a beacon-chain accounting design validation document (pseudocode) and a python project, including pytests to prove the eODS beacon-chain accounting design. # Beacon chain accounting - eODS feature ## 1. Validator balances ### 1.1 Validator records We can add `delegated` boolean entry in the records: ```python class Validator(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals effective_balance: Gwei # Balance at stake slashed: boolean # Status epochs activation_eligibility_epoch: Epoch # When criteria for activation were met activation_epoch: Epoch exit_epoch: Epoch withdrawable_epoch: Epoch # When validator can withdraw funds delegated: boolean # new in eODS ``` ### 1.2 Beacon chain records Actual balance is stored in the `BeaconState` records, as a registry of validators and their balances. We can add a fee `List` structure, mapping each Validator in the registry with its fee rate (`NULL` if `delegated` entry defined in `class Validator` is `False`): ```python # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] fees: List[Fee, VALIDATOR_REGISTRY_LIMIT] ``` ## 2. Delegator balances ### 2.1 BeaconState records We can add beacon chain `class Delegator`: ```python class Delegator(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals delegated_balance: Gwei # Balance at stake # Status epochs activation_epoch: Epoch exit_epoch: Epoch ``` Delegator balances are to be stored in the `BeaconState` records, as a registry of delegators and their balances: We can add Delegators registry inside `BeaconState` ```python # Validators Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] # Delegators Registry - New in eODS delegators: List[Delegator, DELEGATOR_REGISTRY_LIMIT] balances: List[Gwei, DELEGATOR_REGISTRY_LIMIT] ``` ## 3. Beacon-chain accounting design validation project - delegating to validators ### 3.1 classDelegator.py For toy-project purposes, I wrote a toy `class Delegator` and imported it in the code file. The delegator IDs are stored as string, i.e. Validator_no1, Validator_no2, instead of delegator pubkeys ```python class Delegator: pubkey : str balance : float quota : float ``` ### 3.2 Code.py The code file stores delegators' data as a list of `Delegator` instances. If we dig deeper into the implementation, we can observe that the following functions perform balance sheet operations, as follows: - `delegate` - Delegates capital to the `DelegatedValidator`, updates delegator's balance, and recalculates delegators' quotas. - `withdraw` - Allows a delegator to withdraw up to their balance, updating their quota - `adjust_DelegatedValidator_balance` - Adjusts the total DelegatedValidator value (profit/loss) without changing individual quotas. - `_recalculate_quotas` - Recalculates and updates all delegator quotas to ensure the sum is 1. Helper functions - `get_delegator_quota` - Returns the delegator's quota as a percentage of the total balance of the delegated validator. - `get_all_quotas` - Returns a list with all delegators' quotas. - `get_delegator_index_by_pubkey` - Returns de index of a delegator with a given pubkey ```python import math from classDelegator import Delegator class DelegatedValidator: def __init__(self): self.total_balance = 0 # Validator's initial effective balance self.delegators = [] # Stores delegators' data as a list of delegator objects def delegate(self, pubkey, amount): """delegates capital to the DelegatedValidator, updates delegator's balance, and recalculates quotas.""" if amount <= 0: raise ValueError("delegate amount must be positive.") # Update delegator's balance index = self.get_delegator_index_by_pubkey(pubkey) if type(index) == int: self.delegators[index].balance += amount else: # append new delegator to list delegator = Delegator() delegator.pubkey = pubkey delegator.balance = amount delegator.quota = 0 self.delegators.append(delegator) # Update total DelegatedValidator value and recalculate quotas self.total_balance += amount self._recalculate_quotas() def withdraw(self, delegator_index, amount): """Allows a delegator to withdraw up to their balance, updating their quota.""" if delegator_index not in self.delegators: raise ValueError("delegator not found.") if amount <= 0: raise ValueError("Withdrawal amount must be positive.") # Ensure the delegator has sufficient balance delegator_balance = self.delegators[delegator_index]['balance'] if amount > delegator_balance: raise ValueError(f"Amount exceeds delegator's balance: {delegator_balance:.2f}") # Adjust delegator's balance and total DelegatedValidator value self.delegators[delegator_index]['balance'] -= amount self.total_balance -= amount # Remove delegator if balance reaches zero if self.delegators[delegator_index]['balance'] == 0: del self.delegators[delegator_index] # Recalculate quotas to ensure they sum to 1 self._recalculate_quotas() def adjust_DelegatedValidator_balance(self, change_in_value): """Adjusts the total DelegatedValidator value (profit/loss) without changing individual quotas.""" self.total_balance += change_in_value if self.total_balance < 0: raise ValueError("DelegatedValidator total value cannot be negative.") for delegator in self.delegators: delegator.balance += change_in_value * delegator.quota # Quotas remain the same; balances are recalculated. def _recalculate_quotas(self): """Recalculates and updates all delegator quotas to ensure the sum is 1.""" if self.total_balance == 0: # If the total DelegatedValidator value is zero, reset all quotas to zero for delegator in self.delegators: delegator.quota = 0 else: # Recalculate quotas as a fraction of the total DelegatedValidator value for delegator in self.delegators: delegator.quota = delegator.balance / self.total_balance # Confirm quotas sum to 1 total_quota = sum(delegator.quota for delegator in self.delegators) if abs(total_quota - 1) > 1e-6: raise AssertionError("Sum of quotas should be 1 but is {:.6f}".format(total_quota)) def get_delegator_quota(self, delegator_index): """Returns the delegator's quota as a percentage of the total DelegatedValidator.""" if delegator_index not in self.delegators: raise ValueError("delegator not found.") return self.delegators[delegator_index]['quota'] def get_delegator_index_by_pubkey(self, pubkey): """Returns de index of a delegator with a given pubkey""" for index, delegators in enumerate(self.delegators): if delegators.pubkey == pubkey: return index def get_all_quotas(self): """Returns a dictionary with all ' quotas.""" return {delegator_index: data['quota'] for delegator_index, data in self.delegators.items()} ``` ### 3.3 Tests.py The following 4 tests are OK: - `test_add_delegate` - Add a delegator and set its balance. - `test_add_multiple_delegates` - Add multiple delegators and set their balance. - `test_adjust_DelegatedValidator_value_quota` - Adjust the DelegatedValidator value and have the delegators balance updated. - `test_adjust_DelegatedValidator_balance_quota` - Adjust the DelegatedValidator balance and have the delegators' quota unchanged. ```python import unittest from accounting import DelegatedValidator class TestSum(unittest.TestCase): def test_add_delegate(self): """ I should be able to add a delegator and set its balance """ delegatedvalidator = DelegatedValidator() delegatedvalidator.delegate('Delegator_no1', 12) value = delegatedvalidator.delegators[0].balance self.assertEqual(value, 12) def test_add_multiple_delegates(self): """ I should be able to add multiple delegators and set their balance """ delegatedvalidator = DelegatedValidator() delegatedvalidator.delegate('Delegator_no1', 12) delegatedvalidator.delegate('Delegator_no2', 55) valueDelegator_no1 = delegatedvalidator.delegators[0].balance valueDelegator_no2 = delegatedvalidator.delegators[1].balance self.assertEqual(valueDelegator_no1, 12) self.assertEqual(valueDelegator_no2, 55) def test_adjust_DelegatedValidator_value(self): """ I should be able to adjust the DelegatedValidator balance and have the delegators balance updated """ delegatedvalidator = DelegatedValidator() delegatedvalidator.delegate('Delegator_no1', 12) delegatedvalidator.delegate('Delegator_no2', 12) delegatedvalidator.adjust_DelegatedValidator_balance(100) valueDelegator_no1 = delegatedvalidator.delegators[0].balance self.assertEqual(valueDelegator_no1, 62) def test_adjust_DelegatedValidator_balance_quota(self): """ I should be able to adjust the DelegatedValidator balance and have the delegators quota unchanged """ delegatedvalidator = DelegatedValidator() delegatedvalidator.delegate('Delegator_no1', 10) delegatedvalidator.delegate('Delegator_no2', 10) quotaDelegator_no1 = delegatedvalidator.delegators[0].quota self.assertEqual(quotaDelegator_no1, 0.5) delegatedvalidator.adjust_DelegatedValidator_balance(100) quotaDelegator_no1 = delegatedvalidator.delegators[0].quota self.assertEqual(quotaDelegator_no1, 0.5) if __name__ == '__main__': unittest.main() ``` ![image](https://hackmd.io/_uploads/SkM80pXeJx.png) ## 4. Week 20 planning - For the first iteration of the beacon-chain accounting design validation project, I used a key,value pair structure to store delegtors' data. I then prefered to switch to a list sequence insetead, as it's less resources demanding. I refactored the code needed to pass the 4 tests, and I plan to fix the code in the remaining functions (marked as BROKEN), during next week. ## 5. Week 21 planning Write final dev update & project presentation