# Beacon chain accounting - eODS feature As I mentioned in the [Accounting](https://hackmd.io/@kboomro/r1csps2MR#ACCOUNTING) section of eODS design notes, beacon chain accounting already exists under the current protocol, in the form of beacon chain operations, or beacon state mutators, e.g. process_deposit, increase_balance, etc. Since Accounting is an important part of eODS model, it proved useful to group these functions in a virtual group based on functionality (i.e. accounting) rather than beacon-chain specification appartenance. [This document](https://hackmd.io/ZQocZMA9RyCZNlNHPGMeRg#Beacon-chain-accounting-under-current-Ethereum-protocol) is a study I made on beacon chain accounting under the current Ethereum protocol. The following is a design validation document (pseudocode), with a python project and pytests attached, in order to prove the beacon-chain accounting model I proposed for the 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 state records Validators' 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): #### BeaconState validators registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] fees: List[Fee, VALIDATOR_REGISTRY_LIMIT] ## 2. Delegator balances ### 2.1 Beacon-chain 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, similar to validator registry ### BeaconState delegators registry - new in eODS ```python delegators: List[Delegator, DELEGATOR_REGISTRY_LIMIT] balances: List[Gwei, DELEGATOR_REGISTRY_LIMIT] ``` ## 3. Beacon-chain accounting design validation project - delegating to validators I went for a Test-driven Development approach for writing the model validation project. The minimal requirements of functionality proven by tests: * `test_initiate_Delegator` - Add a delegator to registry and set its balance (deposit). * `test_add_multiple_Delegators` - Add multiple delegators to delegators' registry and set their balance (deposit). * `test_delegate` - Delegate to delegated validators and update: * the actual balance of delegated validators (credit balance with delegated `amount`) * the actual balance of delegators (debit delegator's actual balance with delegated `amount`) * total delegated balance inside a validator * the quotas of the delegators that delegated towards those particular validators. * `test_adjust_accounting` - Adjust the validator's balance (due to profit / loss) and have the delegators' balances updated. * `test_adjust_accounting_keep_quota` - Adjust the validator's balance (due to profit / loss) and have the delegators' quota unchanged. * `test_withdraw` - Withdraw a specified amount from a delegator's balance in a particular validator, and update: * the actual balance of delegated validators (debit calidator's balance with the withdrawn `amount`) * the actual balance of delegators (credit delegator's actual balance with the withdrawn `amount`) * total delegated balance inside that validator * the quotas of the delegators that delegated towards that particular validator. ### 3.1 Tests ```python import unittest from beacon_chain_accounting import DelegatorsRegistry, DelegatedValidator, BeaconChainAccounting class TestSum(unittest.TestCase): def test_initiate_Delegator(self): """ I should be able to add a delegator and set its balance """ delegatorsregistry = DelegatorsRegistry() delegatorsregistry.register_delegator('Delegator_no1') delegatorsregistry.deposit('Delegator_no1', 12) value = delegatorsregistry.delegators_balances[0] self.assertEqual(value, 12) def test_initiate_multiple_Delegators(self): """ I should be able to add multiple delegators and set their balance (deposit) """ delegatorsregistry = DelegatorsRegistry() delegatorsregistry.register_delegator('Delegator_no1') #delegatorsregistry.register_delegator('Delegator_no2') # Intentionally left the first one un-registered to test if `deposit` correctly registers delegators, even if they don't exist delegatorsregistry.deposit('Delegator_no1', 12) delegatorsregistry.deposit('Delegator_no2', 12) value_for_delegator_no1 = delegatorsregistry.delegators_balances[0] value_for_delegator_no2 = delegatorsregistry.delegators_balances[1] self.assertEqual(value_for_delegator_no1, 12) self.assertEqual(value_for_delegator_no2, 12) def test_delegate(self): """ I should be able to delegate to a delegated validator and update the quotas of the delegators that delegated towards that particular validator. """ accounting = BeaconChainAccounting() for _ in range(2): accounting.add_validator('Validator_no'+ f'{str(_ + 1)}') accounting.deposit('Delegator_no1', 500) accounting.deposit('Delegator_no2', 400) # Delegate funds from delegators to validators accounting.delegate("Delegator_no1", "Validator_no1", 100) # Delegator 1 delegates 100 to Validator 1 accounting.delegate("Delegator_no2", "Validator_no1", 200) accounting.delegate("Delegator_no1", "Validator_no2", 50) accounting.delegate("Delegator_no2", "Validator_no2", 150) value = accounting.validators[0].validators_balances self.assertEqual(value, 300) value = accounting.delegators_registry.delegators_balances[0] self.assertEqual(value, 350) value = accounting.delegators_registry.delegators_balances[1] self.assertEqual(value, 50) # Get quotas by delegator across all validators print(accounting.get_all_quotas_by_delegator()) def test_adjust_accounting(self): """ I should be able to adjust the validator's balance (due to profit / loss) and have the delegators' balances updated """ accounting = BeaconChainAccounting() for _ in range(2): accounting.add_validator('Validator_no'+ f'{str(_ + 1)}') accounting.deposit('Delegator_no1', 150) accounting.deposit('Delegator_no2', 350) # Delegate funds from delegators to validators accounting.delegate("Delegator_no1", "Validator_no1", 100) # Delegator 1 delegates 100 to Validator 1 accounting.delegate("Delegator_no2", "Validator_no1", 200) accounting.delegate("Delegator_no1", "Validator_no2", 50) accounting.delegate("Delegator_no2", "Validator_no2", 150) accounting.adjust_DelegatedValidator_balance('Validator_no1', 100) valueDelegator_no1 = accounting.delegators_registry.delegators_balances[0] self.assertEqual(valueDelegator_no1, 0) def test_adjust_accounting_keep_quota(self): #BROKEN """ I should be able to adjust the validator's balance and have the delegators quota unchanged """ accounting = BeaconChainAccounting() accounting.delegate('Delegator_no1', 10) accounting.delegate('Delegator_no2', 10) quotaDelegator_no1 = accounting.delegators[0].quota self.assertEqual(quotaDelegator_no1, 0.5) accounting.adjust_accounting_balance(100) quotaDelegator_no1 = accounting.delegators[0].quota self.assertEqual(quotaDelegator_no1, 0.5) if __name__ == '__main__': unittest.main() ``` ### 3.2 classValidator and classDelegator For the python project purposes, I wrote toy classValidator and classDelegator and imported them in the code file. The validator and the delegator IDs are stored as string, i.e. Validator_no1, Validator_no2, and Delegator_no1, Delegator_no2 respectively, instead of delegator pubkeys, as in the beacon-chain specification. ```python import numpy as np class Validator: def __init__(self, validator_id): self.validator_id = validator_id # Unique identifier for each validator # Custom types Epoch : np.uint64 = 0 Gwei : np.uint64 = 0 BLSPubkey : str # pubkey = BLSPubkey # for the purpose of this pyproject, we work with validator_id instead of BLSPubkey, which will be used in the specs self.withdrawal_credentials: bytes = b'\x00' * 32 # Commitment to pubkey for withdrawals self.effective_balance = Gwei # Balance at stake self.slashed: bool # Status epochs self.activation_eligibility_epoch = Epoch # When criteria for activation were met self.activation_epoch = Epoch self.exit_epoch = Epoch self.withdrawable_epoch = Epoch # When validator can withdraw funds self.delegated: bool # new in eODS ``` ```python import numpy as np class Delegator: def __init__(self, delegator_id): self.delegator_id = delegator_id # Unique identifier for each delegator # Custom types Epoch : np.uint64 = 0 Gwei : np.uint64 = 0 BLSPubkey: str # self.pubkey = BLSPubkey # for the purpose of this pyproject, we work with delegator_id instead of BLSPubkey, which will be used in the specs self.withdrawal_credentials: bytes = b'\x00' * 32 # Commitment to pubkey for withdrawals self.delegated_balance = Gwei # Balance at stake # Status epochs self.activation_epoch = Epoch self.exit_epoch = Epoch ``` ### 3.3 Beacon-chain-accounting code The code file stores validators' and delegators' data as a list of `Validator` and `Delegator` class instances. Looking deeper into the implementation, we can observe that the following functions perform balance sheet operations, as follows: * `register_delegator` - Registers a delegator if not already registered and returns the delegator index * `deposit` - Adds an amount to the balance of an existing or newly registered delegator * `delegate` - Delegates capital to a d elegated validator, updates validator's and delegator's balance, and recalculates delegators' quotas. * `withdraw` - Withdraws a specified amount from a delegator's balance in this validator, returning it to the delegator's available balance, updating delegators' quotas. * `adjust_DelegatedValidator_balance` - Adjusts the total DelegatedValidator value (due to profit/loss) without changing individual delegators' quotas. *`calculate_quotas` - Calculate and return the quota for each delegator in this validator. Helper functions: * `get_all_quotas_by_delegator` - Returns a list of delegators with their quotas across all validators. Each entry contains the delegator ID and a list of their quotas across validators. * `get_validator_index` - Get a validator's index in `List[Validator]`, based on its given ID ```python from typing import List import numpy as np from classDelegator import Delegator from classValidator import Validator # State list lengths VALIDATOR_REGISTRY_LIMIT = 100 # Validator registry size limit DELEGATOR_REGISTRY_LIMIT = 100 # Delegator registry size limit # Custom types Gwei = np.uint64 Fee = np.uint Quota = np.uint delegator_index = np.uint validator_index = np.uint class DelegatorsRegistry: delegators: List[Delegator] # Stores delegators' data as a list of Delegator instances. delegators_balances: List[Gwei] # List of Gwei delegators' balances delegators_quotas: List[Quota] # List of delegators' quotas def __init__(self): # Delegators lists initialization self.delegators: List[Delegator] = [] self.delegators_balances: List[Gwei] = [] # Max size: DELEGATOR_REGISTRY_LIMIT self.delegators_quotas: List[Quota] = [] def register_delegator(self, delegator_id): """Registers a delegator if not already registered and returns the delegator index.""" for index, delegator in enumerate(self.delegators): if delegator.delegator_id == delegator_id: return index # Delegator already exists; return the index # Register new delegator with a zero balance new_delegator = Delegator(delegator_id) self.delegators.append(new_delegator) self.delegators_balances.append(0) # Initial balance is 0 return len(self.delegators) - 1 # Return the index of the newly added delegator def deposit(self, delegator_id, amount): """Adds an amount to the balance of an existing or newly registered delegator.""" if amount <= 0: raise ValueError("Deposit amount must be positive.") # Register the delegator if they don't exist and get their index delegator_index = self.register_delegator(delegator_id) # Add the deposit amount to the delegator's balance self.delegators_balances[delegator_index] += amount return delegator_index class DelegatedValidator: delegators_balances: List[Gwei] # List of Gwei delegators' balances validators: List[Validator] # Stores validators' data as a list of Validator instances. validators_balances: List[Gwei] # List of Gwei validators' balances validators_fees: List[Fee] # List of Gwei validators' balances def __init__(self, validator_id): # Valaidators lists initialization self.validators: List[Validator] = [] self.validators_balances: List[Gwei] = 0 # Max size: VALIDATOR_REGISTRY_LIMIT #BROKEN self.validators_fees: List[Fee] = [] self.validator = Validator(validator_id) self.delegators_balances = [] # Track delegated balances for each delegator in this validator def delegate(self, delegator_index, amount): """Transfers capital from a delegator to the validator, updating both balances.""" if amount <= 0: raise ValueError("Delegation amount must be positive.") # Ensure the validator's balance list is large enough to accommodate the delegator while len(self.delegators_balances) <= delegator_index: self.delegators_balances.append(0) # Deduct from the delegator's balance and add to the validator's balance self.delegators_balances[delegator_index] += amount self.validators_balances += amount # Return updated quotas for this validator return self.calculate_quotas() def withdraw(self, delegator_index, amount): """Withdraws a specified amount from a delegator's balance in this validator, returning it to the delegator's available balance.""" if amount <= 0: raise ValueError("Withdrawal amount must be positive.") # Ensure the withdrawal amount does not exceed the delegator's balance in the validator if amount > self.delegators_balances[delegator_index]: raise ValueError("Withdrawal amount exceeds the delegator's delegated balance in this validator.") # Add to the delegator's balance and deduct from the validator's balance self.delegators_balances[delegator_index] -= amount self.validators_balances -= amount # Recalculate and return updated quotas after the withdrawal return self.calculate_quotas() def calculate_quotas(self): """Calculate and return the quota for each delegator in this validator.""" total_balance = self.validators_balances # Avoid division by zero if there is no balance if total_balance == 0: return [(index, 0.0) for index in range(len(self.delegators_balances))] # Calculate quotas for each delegator quotas = [(index, balance / total_balance) for index, balance in enumerate(self.delegators_balances)] return quotas class BeaconChainAccounting: def __init__(self): self.validators = [] # List of DelegatedValidator instances self.delegators_registry = DelegatorsRegistry() # Registry to manage delegators def add_validator(self, validator_id): """Add a new validator with a specific ID.""" validator = DelegatedValidator(validator_id) self.validators.append(validator) return validator_id # Return the validator_id of the newly added validator def get_validator_index(self, validator_id): """Helper function to find a validator's index by its ID.""" for index, validator in enumerate(self.validators): if validator.validator.validator_id == validator_id: return index raise ValueError("Validator not found.") def deposit(self, delegator_id, amount): """Deposit capital from outside the system to a delegator's balance.""" return self.delegators_registry.deposit(delegator_id, amount) def delegate(self, delegator_id, validator_id, amount): """Transfer capital from a delegator's balance to a specific validator's balance.""" # Get validator index by its ID validator_index = self.get_validator_index(validator_id) # Ensure the delegator exists and retrieve their index delegator_index = self.delegators_registry.register_delegator(delegator_id) # Ensure the delegator has sufficient balance to delegate if amount > self.delegators_registry.delegators_balances[delegator_index]: raise ValueError("Delegation amount exceeds the delegator's available balance.") # Deduct the delegation amount from the delegator's remaining balance self.delegators_registry.delegators_balances[delegator_index] -= amount # Delegate the amount to the validator return self.validators[validator_index].delegate(delegator_index, amount) def adjust_DelegatedValidator_balance(self, validator_id, change_in_value): #BROKEN """Adjusts the total DelegatedValidator value (profit/loss) without changing individual quotas.""" # Get validator index by its ID validator_index = self.get_validator_index(validator_id) self.validators[validator_index].validators_balances += change_in_value if self.validators[validator_index].validators_balances < 0: raise ValueError("DelegatedValidator total value cannot be negative.") for delegator_index in enumerate(self.validators[validator_index].delegators_balances): self.delegators_registry.delegators_balances[delegator_index] += change_in_value * self.delegators_registry.delegators_quotas[delegator_index] # Quotas remain the same; balances are recalculated. def withdraw(self, delegator_id, validator_id, amount): """Withdraw capital from a validator's balance back to the delegator's available balance.""" # Get validator index by its ID validator_index = self.get_validator_index(validator_id) # Get the delegator's index from the registry delegator_index = self.delegators_registry.register_delegator(delegator_id) # Perform the withdrawal from the specified validator result = self.validators[validator_index].withdraw(delegator_index, amount) # Add the withdrawn amount back to the delegator's balance self.delegators_registry.delegators_balances[delegator_index] += amount return result def get_all_quotas_by_delegator(self): """ Return a list of delegators with their quotas across all validators. Each entry contains the delegator ID and a list of their quotas across validators. """ delegators_quotas = [] # Iterate over each delegator in the registry for delegator_index, delegator in enumerate(self.delegators_registry.delegators): quotas = [] for validator in self.validators: # If the delegator exists in the current validator, get their quota if delegator_index < len(validator.delegators_balances): quota = validator.calculate_quotas()[delegator_index][1] else: quota = 0.0 # Quota is 0 if the delegator has no balance in this validator quotas.append(quota) # Append delegator ID and their quotas across validators delegators_quotas.append((delegator.delegator_id, quotas)) return delegators_quotas ``` ## Resoucres: * [Beacon-chain-accounting Github repository](https://github.com/gorondan/beacon-chain-accounting)