Try   HackMD

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:

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):

# 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:

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


# 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

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
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.
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

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