@ -23,9 +23,11 @@ defmodule Explorer.Staking.ContractState do
:epoch_number ,
:epoch_start_block ,
:is_snapshotting ,
:last_change_block ,
:max_candidates ,
:min_candidate_stake ,
:min_delegator_stake ,
:seen_block ,
:snapshotted_epoch_number ,
:staking_allowed ,
:staking_contract ,
@ -36,11 +38,14 @@ defmodule Explorer.Staking.ContractState do
:validator_set_contract
]
# frequency in blocks
# token renewal frequency in blocks
@token_renew_frequency 10
defstruct [
:seen_block ,
:eth_blocknumber_pull_interval ,
:eth_subscribe_max_delay ,
:snapshotting_finished ,
:timer ,
:contracts ,
:abi
]
@ -69,13 +74,22 @@ defmodule Explorer.Staking.ContractState do
] )
Subscriber . to ( :last_block_number , :realtime )
Subscriber . to ( :stake_snapshotting_finished )
staking_abi = abi ( " StakingAuRa " )
validator_set_abi = abi ( " ValidatorSetAuRa " )
block_reward_abi = abi ( " BlockRewardAuRa " )
token_abi = abi ( " Token " )
staking_contract_address = Application . get_env ( :explorer , __MODULE__ ) [ :staking_contract_address ]
module_env = Application . get_env ( :explorer , __MODULE__ )
# eth_blockNumber pull interval, in milliseconds
eth_blocknumber_pull_interval = String . to_integer ( module_env [ :eth_blocknumber_pull_interval ] )
# eth_subscribe max delay to switch to eth_blockNumber, in seconds
eth_subscribe_max_delay = String . to_integer ( module_env [ :eth_subscribe_max_delay ] )
staking_contract_address = module_env [ :staking_contract_address ]
# 2d21d217 = keccak256(erc677TokenContract())
erc_677_token_contract_signature = " 2d21d217 "
@ -100,7 +114,10 @@ defmodule Explorer.Staking.ContractState do
} )
state = % __MODULE__ {
seen_block : 0 ,
eth_blocknumber_pull_interval : eth_blocknumber_pull_interval ,
eth_subscribe_max_delay : eth_subscribe_max_delay ,
snapshotting_finished : false ,
timer : nil ,
contracts : %{
staking : staking_contract_address ,
validator_set : validator_set_contract_address ,
@ -112,6 +129,8 @@ defmodule Explorer.Staking.ContractState do
:ets . insert ( @table_name ,
block_reward_contract : %{ abi : block_reward_abi , address : block_reward_contract_address } ,
is_snapshotting : false ,
last_change_block : 0 ,
seen_block : 0 ,
snapshotted_epoch_number : - 1 ,
staking_contract : %{ abi : staking_abi , address : staking_contract_address } ,
token_contract : %{ abi : token_abi , address : token_contract_address } ,
@ -123,58 +142,135 @@ defmodule Explorer.Staking.ContractState do
end
def handle_continue ( _ , state ) do
# if eth_subscribe doesn't work during the first eth_subscribe_max_delay seconds
# after server start, use eth_blockNumber
timer = Process . send_after ( self ( ) , :eth_subscribe_stopped , state . eth_subscribe_max_delay * 1000 )
{ :noreply , %{ state | timer : timer } }
end
# handles an event about snapshotting finishing
def handle_info ( { :chain_event , :stake_snapshotting_finished } , state ) do
{ :noreply , %{ state | snapshotting_finished : true } }
end
# received when eth_subscribe is stopped
def handle_info ( :eth_subscribe_stopped , state ) do
state =
if Process . read_timer ( state . timer ) == false do
{ microseconds , state } =
:timer . tc (
fn st -> fetch_state ( st , get_current_block_number ( ) ) end ,
[ state ]
)
# sleep up to eth_blocknumber_pull_interval milliseconds before the next eth_blockNumber request
Process . send_after (
self ( ) ,
:eth_subscribe_stopped ,
max ( state . eth_blocknumber_pull_interval - round ( microseconds / 1000 ) , 0 )
)
state
else
state
end
{ :noreply , state }
end
@doc " Handles new blocks and decides to fetch fresh chain info "
# catches a new block number from eth_subscribe
def handle_info ( { :chain_event , :last_block_number , :realtime , block_number } , state ) do
if block_number > state . seen_block do
# read general info from the contracts (including pool list and validator list)
global_responses =
ContractReader . perform_requests ( ContractReader . global_requests ( block_number ) , state . contracts , state . abi )
epoch_very_beginning = global_responses . epoch_start_block == block_number + 1
if global_responses . epoch_number > get ( :epoch_number ) and not epoch_very_beginning and state . seen_block > 0 do
# if the previous staking epoch finished and we have blocks gap,
# call fetch_state in a loop until the blocks gap is closed
loop_block_start = state . seen_block + 1
loop_block_end = block_number - 1
if loop_block_end >= loop_block_start do
for bn <- loop_block_start . . loop_block_end do
gr = ContractReader . perform_requests ( ContractReader . global_requests ( bn ) , state . contracts , state . abi )
fetch_state ( state . contracts , state . abi , gr , bn , gr . epoch_start_block == bn + 1 )
end
end
end
if state . timer != nil do
Process . cancel_timer ( state . timer )
end
fetch_state ( state . contracts , state . abi , global_responses , block_number , epoch_very_beginning )
{ :noreply , %{ state | seen_block : block_number } }
state = fetch_state ( state , block_number )
timer = Process . send_after ( self ( ) , :eth_subscribe_stopped , state . eth_subscribe_max_delay * 1000 )
{ :noreply , %{ state | timer : timer } }
end
# handles new block and decides to fetch fresh chain info
defp fetch_state ( state , block_number ) do
if block_number <= get ( :seen_block ) do
state
else
{ :noreply , state }
fetch_state_internal ( state , block_number )
end
end
defp fetch_state ( contracts , abi , global_responses , block_number , epoch_very_beginning ) do
validator_min_reward_percent =
get_validator_min_reward_percent ( global_responses . epoch_number , block_number , contracts , abi )
defp fetch_state_internal ( state , block_number ) do
# read general info from the contracts (including pool list and validator list)
global_responses =
ContractReader . perform_requests ( ContractReader . global_requests ( block_number ) , state . contracts , state . abi )
is_validator = Enum . into ( global_responses . validators , %{ } , & { address_bytes_to_string ( &1 ) , true } )
epoch_very_beginning = global_responses . epoch_start_block == block_number + 1
start_snapshotting = start_snapshotting? ( global_responses )
start_snapshotting =
global_responses . epoch_number > get ( :snapshotted_epoch_number ) && global_responses . epoch_number > 0 &&
not get ( :is_snapshotting )
# determine if something changed in contracts state since the previous seen block.
# if something changed or the `fetch_state` function is called for the first time
# or we are at the beginning of staking epoch or snapshotting recently finished
# then we should update database
last_change_block =
max ( global_responses . staking_last_change_block , global_responses . validator_set_last_change_block )
active_pools_length = Enum . count ( global_responses . active_pools )
first_fetch = get ( :epoch_end_block , 0 ) == 0
should_update_db =
start_snapshotting or state . snapshotting_finished or first_fetch or last_change_block > get ( :last_change_block )
# save the general info to ETS (excluding pool list and validator list)
settings =
global_responses
|> get_settings ( validator_min_reward_percent , block_number )
|> Enum . concat ( active_pools_length : active_pools_length )
set_settings ( global_responses , state , block_number , last_change_block )
:ets . insert ( @table_name , settings )
if epoch_very_beginning or start_snapshotting do
# if the block_number is the latest block of the finished staking epoch
# or we are starting Blockscout server, the BlockRewardAuRa contract balance
# could increase before (without Mint/Transfer events),
# so we need to update its balance in database
update_block_reward_balance ( block_number )
end
# we should update database as something changed in contracts state
if should_update_db do
update_database (
epoch_very_beginning ,
start_snapshotting ,
global_responses ,
state . contracts ,
state . abi ,
block_number
)
end
# notify the UI about a new block
data = %{
active_pools_length : get ( :active_pools_length ) ,
block_number : block_number ,
epoch_end_block : global_responses . epoch_end_block ,
epoch_number : global_responses . epoch_number ,
max_candidates : global_responses . max_candidates ,
staking_allowed : global_responses . staking_allowed ,
staking_token_defined : get ( :token , nil ) != nil ,
validator_set_apply_block : global_responses . validator_set_apply_block
}
Publisher . broadcast ( [ { :staking_update , data } ] , :realtime )
if state . snapshotting_finished do
%{ state | snapshotting_finished : false }
else
state
end
end
defp start_snapshotting? ( global_responses ) do
global_responses . epoch_number > get ( :snapshotted_epoch_number ) && global_responses . epoch_number > 0 &&
not get ( :is_snapshotting )
end
defp update_database ( epoch_very_beginning , start_snapshotting , global_responses , contracts , abi , block_number ) do
is_validator = Enum . into ( global_responses . validators , %{ } , & { address_bytes_to_string ( &1 ) , true } )
# form the list of validator pools
validators =
@ -220,14 +316,7 @@ defmodule Explorer.Staking.ContractState do
# call `BlockReward.delegatorShare` function for each delegator
# to get their reward share of the pool (needed for the `Delegators` list in UI)
delegator_responses =
Enum . reduce ( staker_responses , %{ } , fn { { pool_staking_address , staker_address , _is_active } = key , value } , acc ->
if pool_staking_address != staker_address do
Map . put ( acc , key , value )
else
acc
end
end )
delegator_responses = get_delegator_responses ( staker_responses )
delegator_reward_responses =
get_delegator_reward_responses (
@ -239,9 +328,6 @@ defmodule Explorer.Staking.ContractState do
block_number
)
# calculate total amount staked into all active pools
staked_total = Enum . sum ( for { _ , pool } <- pool_staking_responses , pool . is_active , do : pool . total_staked_amount )
# calculate likelihood of becoming a validator on the next epoch
[ likelihood_values , total_likelihood ] = global_responses . pools_likelihood
# array of pool addresses (staking addresses)
@ -252,7 +338,20 @@ defmodule Explorer.Staking.ContractState do
snapshotted_epoch_number = get ( :snapshotted_epoch_number )
# form entries for writing to the `staking_pools` table in DB
# form entries for writing to the `staking_pools_delegators` table in DB
delegator_entries = get_delegator_entries ( staker_responses , delegator_reward_responses )
# perform SQL queries
{ :ok , _ } =
Chain . import ( %{
staking_pools_delegators : %{ params : delegator_entries } ,
timeout : :infinity
} )
# form entries for writing to the `staking_pools` table in DB.
# !!! it's important to do this AFTER updating `staking_pools_delegators`
# !!! table because the `get_pool_entries` function requires fresh
# !!! info about delegators of validators from the `staking_pools_delegators` table
pool_entries =
get_pool_entries ( %{
pools : pools ,
@ -263,25 +362,16 @@ defmodule Explorer.Staking.ContractState do
global_responses : global_responses ,
snapshotted_epoch_number : snapshotted_epoch_number ,
likelihood : likelihood ,
total_likelihood : total_likelihood ,
staked_total : staked_total
total_likelihood : total_likelihood
} )
# form entries for writing to the `staking_pools_delegators` table in DB
delegator_entries = get_delegator_entries ( staker_responses , delegator_reward_responses )
# perform SQL queries
{ :ok , _ } =
Chain . import ( %{
staking_pools : %{ params : pool_entries } ,
staking_pools_delegators : %{ params : delegator_entries } ,
timeout : :infinity
} )
if epoch_very_beginning or start_snapshotting do
at_start_snapshotting ( block_number )
end
if start_snapshotting do
do_start_snapshotting (
epoch_very_beginning ,
@ -293,23 +383,31 @@ defmodule Explorer.Staking.ContractState do
mining_to_staking_address
)
end
end
# notify the UI about a new block
data = %{
active_pools_length : active_pools_length ,
block_number : block_number ,
epoch_end_block : global_responses . epoch_end_block ,
epoch_number : global_responses . epoch_number ,
max_candidates : global_responses . max_candidates ,
staking_allowed : global_responses . staking_allowed ,
staking_token_defined : get ( :token , nil ) != nil ,
validator_set_apply_block : global_responses . validator_set_apply_block
}
defp get_current_block_number do
json_rpc_named_arguments = Application . get_env ( :explorer , :json_rpc_named_arguments )
Publisher . broadcast ( [ { :staking_update , data } ] , :realtime )
result =
%{ id : 0 , method : " eth_blockNumber " , params : [ ] }
|> EthereumJSONRPC . request ( )
|> EthereumJSONRPC . json_rpc ( json_rpc_named_arguments )
case result do
{ :ok , response } ->
response
|> String . replace_leading ( " 0x " , " " )
|> String . to_integer ( 16 )
_ ->
0
end
end
defp get_settings ( global_responses , validator_min_reward_percent , block_number ) do
defp get_settings ( global_responses , state , block_number ) do
validator_min_reward_percent =
get_validator_min_reward_percent ( global_responses . epoch_number , block_number , state . contracts , state . abi )
base_settings = get_base_settings ( global_responses , validator_min_reward_percent )
update_token =
@ -324,6 +422,17 @@ defmodule Explorer.Staking.ContractState do
end
end
defp set_settings ( global_responses , state , block_number , last_change_block ) do
settings =
global_responses
|> get_settings ( state , block_number )
|> Enum . concat ( active_pools_length : Enum . count ( global_responses . active_pools ) )
|> Enum . concat ( last_change_block : last_change_block )
|> Enum . concat ( seen_block : block_number )
:ets . insert ( @table_name , settings )
end
defp get_mining_to_staking_address ( validators , contracts , abi , block_number ) do
validators . all
|> Enum . map ( & ContractReader . staking_by_mining_request ( &1 , block_number ) )
@ -390,6 +499,16 @@ defmodule Explorer.Staking.ContractState do
|> ContractReader . perform_grouped_requests ( pool_staking_keys , contracts , abi )
end
defp get_delegator_responses ( staker_responses ) do
Enum . reduce ( staker_responses , %{ } , fn { { pool_staking_address , staker_address , _is_active } = key , value } , acc ->
if pool_staking_address != staker_address do
Map . put ( acc , key , value )
else
acc
end
end )
end
defp get_delegator_reward_responses (
delegator_responses ,
pool_staking_responses ,
@ -543,9 +662,11 @@ defmodule Explorer.Staking.ContractState do
global_responses : global_responses ,
snapshotted_epoch_number : snapshotted_epoch_number ,
likelihood : likelihood ,
total_likelihood : total_likelihood ,
staked_total : staked_total
total_likelihood : total_likelihood
} ) do
# total amount staked into all active pools
staked_total = Enum . sum ( for { _ , pool } <- pool_staking_responses , pool . is_active , do : pool . total_staked_amount )
Enum . map ( pools , fn pool_staking_address ->
staking_resp = pool_staking_responses [ pool_staking_address ]
mining_resp = pool_mining_responses [ pool_staking_address ]
@ -565,6 +686,15 @@ defmodule Explorer.Staking.ContractState do
0
end
is_unremovable = address_bytes_to_string ( pool_staking_address ) == global_responses . unremovable_validator
likelihood_value =
if get ( :active_pools_length ) > global_responses . max_validators and not is_unremovable do
ratio ( likelihood [ pool_staking_address ] || 0 , total_likelihood )
else
100
end
%{
staking_address_hash : pool_staking_address ,
delegators_count : delegators_count ,
@ -575,11 +705,11 @@ defmodule Explorer.Staking.ContractState do
0
end ,
validator_reward_ratio : Float . floor ( candidate_reward_resp . validator_share / 10_000 , 2 ) ,
likelihood : ratio ( likelihood [ pool_staking_address ] || 0 , total_likelihood ) ,
likelihood : likelihood_value ,
validator_reward_percent : staking_resp . validator_reward_percent / 10_000 ,
is_deleted : false ,
is_validator : is_validator ,
is_unremovable : address_bytes_to_str ing ( pool _staking_address ) == global_responses . unremovable_validator ,
is_unremovable : is _unremovable ,
ban_reason : binary_to_string ( mining_resp . ban_reason )
}
|> Map . merge (
@ -603,7 +733,7 @@ defmodule Explorer.Staking.ContractState do
end )
end
defp at_start_snapshotting ( block_number ) do
defp update_block_reward_balance ( block_number ) do
# update ERC balance of the BlockReward contract
token = get ( :token )