Table of Contents

​​​​import pandas as pd
​​​​import numpy as np

Assumptions & Key Considerations

1. Base reward per increment

The number of validators considered is 491,875 (As on 23rd Dec’22) and the corresponding base reward per increment is 510.

​​​​num_validators = 491875
​​​​base_reward_factor = 64 #constant

​​​​base_reward_ = (10** 9 * base_reward_factor) / np.sqrt(32*10**9 * num_validators) 
​​​​print(base_reward_)

​​​​base_reward = base_reward_ 


​​​​Ws=14
​​​​Wt=26
​​​​Wh=14
​​​​Wsum = 64

2. Performance Metrics for Permissionless node operators - bottom performers

Basis of Permissionless node operators - bottom performers

  • Considered the bottom 20th percentile of deposit addresses from rated.network, arranged by uptime
  • Source: https://www.rated.network/o/Rocketpool?network=mainnet&view=pool
  • The analysis was performed in Nov’22 and to ensure the base data was still relevant at the time of publication of this analysis, the performance metrics of the bottom 20th percentile was recalculated for the last 30 days on 7th Jan’23. Since the validator performance improved from 89.24% to 90.11%, the data for Oct’22 was considered to be sufficient.

Performance Metrics

​​​​Rsth = (Ws+Wt+Wh)/Wsum # percent reward for correct source + target + head 
​​​​Rnst = (-Ws-Wt)/Wsum # penality for wrong source + target 
​​​​Rsnt = (Ws-Wt)/Wsum #Penality for correct source, wrong target 
​​​​Rstnh = (Ws+Wt)/Wsum # Not included due to lack of data on Permissionless node operators - bottom performers

​​​​k= (Rsth*base_reward)/(10**9) 
​​​​k1=(Rnst*base_reward)/(10** 9)
​​​​k2 =(Rsnt*base_reward)/(10** 9)

3. Performance Metrics for Permissionless node operators - Bottom Performers in a DVT Network

DVT significantly improves operator uptime:

DVT technology makes a validator more efficient by improving uptime to 94% when compared to a stand-alone validator with an uptime of 89.2%, given the same level of validator performance.

In a DVT-network, for a validator to perform duties (correctly & timely), atleast 3 out 4 nodes will need to correctly perform duties ie, we will need to calculate:

Probability of atleast 3 nodes performing duties correctly = probability of all 4 nodes performing duties correctly + probability of 3 out of 4 nodes performing duties correctly

4. Penalties and Inactivity Leak

It is assumed that the period of poor performance that attracts penalties and the inactivity leak occursconsecutively in the first set of epochs, and this repeats for 6 cycles of 30 days each.

The implications are as follows:

  1. In all scenarios, has the effect of being more conservative on the effective balance - The effective balance is reduced right at the start leading to a lower balance at the end of the penalty epoch and hence has a lower effective balance on which the rewards are applied.
  2. In case of an inactivity leak, has the effect of assuming the worst-case in the calculation of the inactivity score - This would cover the worst-case scenario with the inactivity score increasing every epoch
  3. Inactivity leak penalty quotient = 16777216

5. Slashing

  1. Simulations for slashing is the same as assuming a validator is offline throughout the entire duration (36 days). However, in addition, there are 2 subtractions to the active balance
  2. The first is initial slash of 1 ETH at the 1st epoch ( It is assumed to occur at the first epoch)
  3. The second is correlation penalty applied at the 4050 epoch (18 days)

Correlation Penalty Calculation: To calculate correlation penalty we need to get the sum of effective balances of slashed validators in the past 36 days. In order to do so we need the probability of slash of a validator in a 36 day window.

Probability of a validator being slashed in a 36 day window

  1. Let p be the probability of a slashing event per epoch per validator
  2. The probability of a validator being slashed in 36 days would be 1 - (1-p)^(36*225)
  3. Calculating probability of a slash event per epoch per validator
  4. There have been 220 slashing events over 168120 epochs of the beacon chain. The number of validators have increased from 21,063 to 490,751
  5. P = (total number of slashing incidents)/(area under the curve)
  6. The area under the graph is calculated by approximating it to a trapezium
  7. P =0.000000005102789204 [5.10E-09]
  8. Based on the value of p the value of Probability of a validator being slashed in a 36 day window comes out to be 0.00004133173837
  9. Correlation penalty =min( B, 3* S/T * B), S/T
  10. S= 0.00004133173837 *n *32
  11. T =n*32
  12. N is the number of validators
  13. S/T =0.00004133173837 ->represented in the code as corr_penallity_sbyt
  14. Correlation penalty then becomes 3*0.00004133173837 *b_eff where b_eff is the effective balance at the 4050th epoch

6. Attestor & Sync Committee Rewards

  1. Proposer rewards are calculated using the formula wp/(wt-wp)*R where R is the value of a single attestation, wt and wp and the total and poposer weights.
  2. Combining the probability of being selected to propose a block with the reward earned we get the total proposer reward for a validator to be 0.01377385714 ETH for 30 days and 0.08264314286 for 180 days.
  3. The sync committee reward per validator per slot is calculated using WyTb/(32512100) where Wy is is the sync reward weight, T is the total number of validators and b is the base reward. Combining this with the probability of being included in the sync committee we get the sync committee rewards to be 0.0034425 ETH for 30 days and 0.020655 ETH for 180 days
  4. The total rewards combining Proposer and sync committee rewards are as follows:

Simulation Code

1. Normal Network Conditions

Normal Network conditions (Non-DVT)

​​​​def thirty_day_cycle_no_inactivity(b,k1,k2,k3,b_eff):
​​​​    b_new=b
​​​​    b_eff=b_eff
​​​​    inactivity_score=0
​​​​    attest_penality=0
​​​​    k=k1
​​​​    for i in range(1,6751):
​​​​        if i <519:
​​​​            k =k1
​​​​            inactivity_score =inactivity_score+4
​​​​            inactivity_score =inactivity_score-16
​​​​        elif i <727:
​​​​            k =k2
​​​​            inactivity_score =inactivity_score+4
​​​​            inactivity_score =inactivity_score-16
​​​​        else :
​​​​            k = k3
​​​​            inactivity_score =inactivity_score-1
​​​​            inactivity_score =inactivity_score-16

​​​​        inactivity_score =max(inactivity_score,0)
​​​​        attest_penality =k*b_eff
​​​​        inactivity_penality = (inactivity_score*b_eff)/(4*16777216)
​​​​        b_new = b_new + attest_penality -inactivity_penality


​​​​        if b_new -b_eff >=1.25:
​​​​            b_eff =b_eff+1
​​​​            b_eff =min(b_eff,32)
​​​​        if b_eff-b_new>=0.25:
​​​​            b_eff =b_eff-1
​​​​            b_eff =max(b_eff,0)

​​​​        b_eff =min(b_eff,32)
​​​​        b_prev =b_new


​​​​    return b_new, b_eff

Simulation Results for Normal Network Conditions

​​​​# Simualtion for Permissionless node operators - Bottom Performers with no inactivity leak 
​​​​
​​​​b_new=32
​​​​b_eff=32
​​​​for i in range(1,7):
​​​​    b_new,b_eff =thirty_day_cycle_no_inactivity(b_new,k1,k2,k,b_eff)
​​​​    print(b_new, i*30,'days')
​​​​    
​​​​Result:
​​​​32.077049457807114 30 days
​​​​32.15409891561533 60 days
​​​​32.231148373423544 90 days
​​​​32.30819783123176 120 days
​​​​32.385247289039974 150 days
​​​​32.46229674684819 180 days

Normal network conditions (DVT Infra)

​​​​def thirty_day_cycle_ssv(b,k1,k2,b_eff):
​​​​    b_new=b
​​​​    b_eff =b_eff
​​​​    inactivity_score=0
​​​​    attest_penality=0
​​​​    k=k1
​​​​    for i in range(1,6751):
​​​​        if i <406:
​​​​            k =k1
​​​​            inactivity_score =inactivity_score+4
​​​​            inactivity_score =inactivity_score-16
​​​​        else :
​​​​            k = k2
​​​​            inactivity_score =inactivity_score-1
​​​​            inactivity_score =inactivity_score-16

​​​​        inactivity_score =max(inactivity_score,0)
​​​​        attest_penality =k*b_eff
​​​​        inactivity_penality = (inactivity_score*b_eff)/(4*16777216)
​​​​        b_new = b_new + attest_penality -inactivity_penality
​​​​        if b_new -b_eff >=1.25:
​​​​            b_eff =b_eff+1
​​​​            b_eff =min(b_eff,32)
​​​​        if b_eff-b_new>=0.25:
​​​​            b_eff =b_eff-1
​​​​            b_eff =max(b_eff,0)

​​​​        b_eff =min(b_eff,32)
​​​​        b_prev =b_new
​​​​    return b_new,b_eff

Simulation results for Normal Network Conditions (DVT Infra)

​​​​b_new=32
​​​​b_eff=32
​​​​for i in range(1,7):
​​​​    b_new,b_eff =thirty_day_cycle_ssv(b_new,k1,k,b_eff)
​​​​    print(b_new, i*30,'days')
​​​​
​​​​Result:
​​​​32.08326024401464 30 days
​​​​32.16652048803072 60 days
​​​​32.2497807320468 90 days
​​​​32.33304097606288 120 days
​​​​32.416301220078964 150 days
​​​​32.499561464095045 180 days

2. Inactivity leak for 7 days

Inactivity Leak for 7 days (Non-DVT)

​​​​def thirty_day_cycle_with_inactivity(b,k1,k2,k3,b_eff):
​​​​    b_new=b
​​​​    b_eff =b

​​​​    inactivity_score=0
​​​​    attest_penality=0
​​​​    k=k1
​​​​    for i in range(1,6751):
​​​​        if i <519 :
​​​​            k =k1
​​​​            inactivity_score =inactivity_score+4
​​​​        elif i <727:
​​​​            k =k2
​​​​            inactivity_score =inactivity_score+4
​​​​        elif i <=1575 :
​​​​            k = 0
​​​​            inactivity_score =inactivity_score-1
​​​​        else:
​​​​            k=k3
​​​​            inactivity_score=inactivity_score-17 # 16 from no inactivity and 1 from being active in epoch participation so total 17

​​​​        inactivity_score =max(inactivity_score,0)
​​​​        attest_penality =k*b_eff
​​​​        inactivity_penality = (inactivity_score*b_eff)/(4*16777216)

​​​​        b_new = b_new + attest_penality -inactivity_penality


​​​​        if b_new -b_eff >=1.25:
​​​​            b_eff =b_eff+1
​​​​            b_eff =min(b_eff,32)
​​​​        if b_eff-b_new>=0.25:
​​​​            b_eff =b_eff-1
​​​​            b_eff =max(b_eff,0)

​​​​        b_eff =min(b_eff,32)



​​​​    return b_new, b_eff

Simulation results for Inactivity leak for 7 days (Non-DVT)

​​​​# Permissionless node operators - Bottom Performers with inactivity leak for 7 days
​​​​b_new=32
​​​​b_eff=32
​​​​for i in range(1,7):
​​​​    if i==1:
​​​​        b_new,b_eff =thirty_day_cycle_with_inactivity(b_new,k1,k2,k,b_eff)
​​​​    else:
​​​​        b_new,b_eff=thirty_day_cycle_no_inactivity(b_new,k1,k2,k,b_eff)
​​​​    print(b_new, i*30,'days')
​​​​    
​​​​    
​​​​    Result:
​​​​    30.545535629329766 30 days
​​​​    30.617769496015566 60 days
​​​​    30.690003362701365 90 days
​​​​    30.762237229387164 120 days
​​​​    30.834471096072964 150 days
​​​​    30.906704962758763 180 days

Inactivity leak for 7 days (DVT Infra)

​​​​def thirty_day_cycle_ssv_inactivity(b,k1,k2,b_eff):
​​​​    b_new=b
​​​​    b_eff=b_eff
​​​​    inactivity_score=0
​​​​    attest_penality=0
​​​​    k=k1
​​​​    for i in range(1,6751):
​​​​        if i <406: #worst case
​​​​            k =k1
​​​​            inactivity_score =inactivity_score+4
​​​​        elif i <1575:#1575 ->7 days
​​​​            k = 0 # during inactivity leak you do not receive rewards
​​​​            inactivity_score =inactivity_score-1
​​​​        else:
​​​​            k = k2 #good case
​​​​            inactivity_score =inactivity_score-17

​​​​        inactivity_score =max(inactivity_score,0)
​​​​        attest_penality =k*b_eff
​​​​        inactivity_penality = (inactivity_score*b_eff)/(4*16777216)
​​​​        b_new = b_new + attest_penality -inactivity_penality

​​​​        if b_new -b_eff >=1.25:
​​​​            b_eff =b_eff+1
​​​​            b_eff =min(b_eff,32)
​​​​        if b_eff-b_new>=0.25:
​​​​            b_eff =b_eff-1
​​​​            b_eff =max(b_eff,0)

​​​​        b_eff =min(b_eff,32)
​​​​        b_prev =b_new


​​​​    return b_new,b_eff

Simulation results for Inactivity leak for 7 days (DVT Infra)

​​​​b_new=32
​​​​b_eff=32
​​​​for i in range(1,7):    
​​​​    if i==1:
​​​​        b_new,b_eff =thirty_day_cycle_ssv_inactivity(b_new,k1,k,b_eff)
​​​​    else:
​​​​        b_new,b_eff=thirty_day_cycle_ssv(b_new,k1,k,b_eff)
​​​​    print(b_new, i*30,'days')
​​​​    
​​​​Result:
​​​​31.34376643054227 30 days
​​​​31.424424791915733 60 days
​​​​31.505083153289196 90 days
​​​​31.585741514662658 120 days
​​​​31.66639987603612 150 days
​​​​31.747058237409583 180 days

Simulation extended for 180 days

​​​​b_new=32
​​​​b_eff=32
​​​​for i in range(1,7):
​​​​    b_new,b_eff =thirty_day_cycle_ssv(b_new,k1,k,b_eff)
​​​​    print(b_new, i*30,'days')
​​​​
​​​​Result:
​​​​32.08326024401464 30 days
​​​​32.16652048803072 60 days
​​​​32.2497807320468 90 days
​​​​32.33304097606288 120 days
​​​​32.416301220078964 150 days
​​​​32.499561464095045 180 days

3. Isolated slashing event

Isolated slashing event without inactivity leak

​​​​def slashing_thirty_day_cycle_no_inactivity(b,k1,epoch,b_eff):
​​​​    b_new=b
​​​​    b_eff=b_eff

​​​​    attest_penality=0
​​​​    k=k1
​​​​    initial_slash=1
​​​​    corr_penality_sbyt=0.00004133173837


​​​​    for i in range(1,epoch):

​​​​        if i==1:
​​​​            b_new=b_new-initial_slash

​​​​        if i==4050:
​​​​            corr_penality =3*corr_penality_sbyt*b_eff
​​​​            b_new =b_new-corr_penality

​​​​        k =k1


​​​​        attest_penality =k*b_eff

​​​​        b_new = b_new + attest_penality

​​​​        if b_new -b_eff >=1.25:
​​​​            b_eff =b_eff+1
​​​​            b_eff =min(b_eff,32)
​​​​        if b_eff-b_new>=0.25:
​​​​            b_eff =b_eff-1
​​​​            b_eff =max(b_eff,0)

​​​​        b_eff =min(b_eff,32)
​​​​        b_prev =b_new



​​​​    return b_new,b_eff

Simulation Results

#30 day
slashing_thirty_day_cycle_no_inactivity(32,k1,30*225,32)

Result:
(30.929450774080273, 31)

#36 day (180 day)
slashing_thirty_day_cycle_no_inactivity(32,k1,36*225,32)

Result:
(30.916107786256845, 31)

Isolated slashing event with inactivity leak for 7 days

def slashing_with_inactivity(b,epoch,k1,b_eff):
    b_new=b
    b_eff =b_eff
    inactivity_score=0
    attest_penality=0
    k=k1
    initial_slash=1
    corr_penality_sbyt=0.00004133173837

    for i in range(1,epoch):
# inactivity leak lasts for 7 days-our assumption
        if i==1:
            b_new=b_new-initial_slash

        if i==4050:
            corr_penality =3*corr_penality_sbyt*b_eff
            print(corr_penality,'-',corr_penality_sbyt,'-',b_eff)
            b_new =b_new-corr_penality   


        if i <1575 :
            k=k1
            inactivity_score =inactivity_score+4

        else:
            k=k1
            inactivity_score =inactivity_score+4
            inactivity_score=inactivity_score-16


        inactivity_score =max(inactivity_score,0)
        attest_penality =k*b_eff
        inactivity_penality = (inactivity_score*b_eff)/(4*16777216)

        b_new = b_new + attest_penality -inactivity_penality

        if b_new -b_eff >=1.25:
            b_eff =b_eff+1
            b_eff =min(b_eff,32)
        if b_eff-b_new>=0.25:
            b_eff =b_eff-1
            b_eff =max(b_eff,0)

        b_eff =min(b_eff,32)
        b_prev =b_new



    return b_new,b_eff

Simulation Results

slashing_with_inactivity(32,30*225,k1,32)

Result:
(28.05433309931474, 28)

slashing_with_inactivity(32,36*225,k1,32)

Result:
(28.042281368379925, 28)

4. Non-Isolated Slashing Event

Non-Isolated slashing without inactivity leak

def slashing_thirty_day_cycle_no_inactivity(b,k1,epoch,b_eff): 
b_new=b
b_eff=b_eff
inactivity_score=0
attest_penality=0
k=k1
initial_slash=1
corr_penality_sbyt=0.00004133173837 ->Normal Slash
corr_penality_sbyt=0.0009689140088817117 #->Staked.us
for i in range(1,epoch):
if i==1:
b_new=b_new-initial_slash
if i==4050:
        corr_penality =3*corr_penality_sbyt*b_eff
        b_new =b_new-corr_penality

k =k1
            
inactivity_score =max(inactivity_score,0)
attest_penality =k*b_eff

inactivity_penality = (inactivity_score*b_new)/(4*50331648)
b_new = b_new + attest_penality

print(i,'-',inactivity_score,'-', round(inactivity_penality,10),'-', round(attest_penality,10), round(b_new,5))

if b_new -b_eff >=1.25:
            b_eff =b_eff+1
            b_eff =min(b_eff,32)
        if b_eff-b_new>=0.25:
            b_eff =b_eff-1
            b_eff =max(b_eff,0)

print(i,'-',b_prev,'-',b_new,'-',b_prev-b_new,'-',b_eff,'-',i,'-',inactivity_score)
        b_eff =min(b_eff,32)
        b_prev =b_new
                
    return b_new,b_eff

Simulation Result

36 day or 180 day slashing

slashing_thirty_day_cycle_no_inactivity(32,k1,36*225,32)
(30.829842635099254, 31)

Non-Isolated slashing with inactivity leak 7 days

def slashing_with_inactivity(b,epoch,k1,b_eff):
    b_new=b
    b_eff =b_eff
    inactivity_score=0
    attest_penality=0
    k=k1
    initial_slash=1
corr_penality_sbyt=0.00004133173837     ->normal slash
    corr_penality_sbyt=0.0009689140088817117  #staked.us
    for i in range(1,epoch):
inactivity leak lasts for 7 days-our assumption
        if i==1:
            b_new=b_new-initial_slash
            
        if i==4050:
            corr_penality =3*corr_penality_sbyt*b_eff
            print(corr_penality,'-',corr_penality_sbyt,'-',b_eff)
            b_new =b_new-corr_penality   
        

        if i <1575 :
            k=k1
            inactivity_score =inactivity_score+4
            
        else:
            k=k1
            inactivity_score =inactivity_score+4
            inactivity_score=inactivity_score-16
     
        
        inactivity_score =max(inactivity_score,0)
        attest_penality =k*b_eff
        inactivity_penality = (inactivity_score*b_eff)/(4*16777216)

        b_new = b_new + attest_penality -inactivity_penality

        if b_new -b_eff >=1.25:
            b_eff =b_eff+1
            b_eff =min(b_eff,32)
        if b_eff-b_new>=0.25:
            b_eff =b_eff-1
            b_eff =max(b_eff,0)

        b_eff =min(b_eff,32)
        b_prev =b_new
        
print(i,'-',inactivity_score,'-',b_new,'-',b_eff)
        
    return b_new,b_eff

Simulation Results

slashing_with_inactivity(32,30*225,k1,32)
(27.976416188591756, 28)

slashing_with_inactivity(32,36*225,k1,32)
(27.96436445765694, 28)
Select a repo