Add stability validators (#9390)
* Add stability validators * Process review comments * Fix testspull/9537/head
parent
fdbb881a18
commit
546b732ac1
@ -0,0 +1,83 @@ |
||||
defmodule BlockScoutWeb.API.V2.ValidatorController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.Chain.Cache.StabilityValidatorsCounters |
||||
alias Explorer.Chain.Stability.Validator, as: ValidatorStability |
||||
|
||||
import BlockScoutWeb.PagingHelper, |
||||
only: [ |
||||
delete_parameters_from_next_page_params: 1, |
||||
stability_validators_state_options: 1, |
||||
validators_stability_sorting: 1 |
||||
] |
||||
|
||||
import BlockScoutWeb.Chain, |
||||
only: [ |
||||
split_list_by_page: 1, |
||||
paging_options: 1, |
||||
next_page_params: 4 |
||||
] |
||||
|
||||
@api_true api?: true |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/validators/stability` endpoint. |
||||
""" |
||||
@spec stability_validators_list(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def stability_validators_list(conn, params) do |
||||
options = |
||||
[ |
||||
necessity_by_association: %{ |
||||
:address => :optional |
||||
} |
||||
] |
||||
|> Keyword.merge(@api_true) |
||||
|> Keyword.merge(paging_options(params)) |
||||
|> Keyword.merge(validators_stability_sorting(params)) |
||||
|> Keyword.merge(stability_validators_state_options(params)) |
||||
|
||||
{validators, next_page} = options |> ValidatorStability.get_paginated_validators() |> split_list_by_page() |
||||
|
||||
next_page_params = |
||||
next_page |
||||
|> next_page_params( |
||||
validators, |
||||
delete_parameters_from_next_page_params(params), |
||||
&ValidatorStability.next_page_params/1 |
||||
) |
||||
|
||||
conn |
||||
|> render(:stability_validators, %{validators: validators, next_page_params: next_page_params}) |
||||
end |
||||
|
||||
@doc """ |
||||
Function to handle GET requests to `/api/v2/validators/stability/counters` endpoint. |
||||
""" |
||||
@spec stability_validators_counters(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def stability_validators_counters(conn, _params) do |
||||
%{ |
||||
validators_counter: validators_counter, |
||||
new_validators_counter: new_validators_counter, |
||||
active_validators_counter: active_validators_counter |
||||
} = StabilityValidatorsCounters.get_counters(@api_true) |
||||
|
||||
conn |
||||
|> json(%{ |
||||
validators_counter: validators_counter, |
||||
new_validators_counter_24h: new_validators_counter, |
||||
active_validators_counter: active_validators_counter, |
||||
active_validators_percentage: |
||||
calculate_active_validators_percentage(active_validators_counter, validators_counter) |
||||
}) |
||||
end |
||||
|
||||
defp calculate_active_validators_percentage(active_validators_counter, validators_counter) do |
||||
if Decimal.compare(validators_counter, Decimal.new(0)) == :gt do |
||||
active_validators_counter |
||||
|> Decimal.div(validators_counter) |
||||
|> Decimal.mult(100) |
||||
|> Decimal.to_float() |
||||
|> Float.floor(2) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,17 @@ |
||||
defmodule BlockScoutWeb.API.V2.ValidatorView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias BlockScoutWeb.API.V2.Helper |
||||
|
||||
def render("stability_validators.json", %{validators: validators, next_page_params: next_page_params}) do |
||||
%{"items" => Enum.map(validators, &prepare_validator(&1)), "next_page_params" => next_page_params} |
||||
end |
||||
|
||||
defp prepare_validator(validator) do |
||||
%{ |
||||
"address" => Helper.address_with_info(nil, validator.address, validator.address_hash, true), |
||||
"state" => validator.state, |
||||
"blocks_validated_count" => validator.blocks_validated |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,205 @@ |
||||
defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do |
||||
use BlockScoutWeb.ConnCase |
||||
|
||||
alias Explorer.Chain.Address |
||||
alias Explorer.Chain.Stability.Validator, as: ValidatorStability |
||||
alias Explorer.Chain.Cache.StabilityValidatorsCounters |
||||
|
||||
if Application.compile_env(:explorer, :chain_type) == "stability" do |
||||
describe "/validators/stability" do |
||||
test "get paginated list of the validators", %{conn: conn} do |
||||
validators = |
||||
insert_list(51, :validator_stability) |
||||
|> Enum.sort_by( |
||||
fn validator -> |
||||
{Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} |
||||
end, |
||||
:desc |
||||
) |
||||
|
||||
request = get(conn, "/api/v2/validators/stability") |
||||
assert response = json_response(request, 200) |
||||
|
||||
request_2nd_page = get(conn, "/api/v2/validators/stability", response["next_page_params"]) |
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
check_paginated_response(response, response_2nd_page, validators) |
||||
end |
||||
|
||||
test "sort by blocks_validated asc", %{conn: conn} do |
||||
validators = |
||||
for _ <- 0..50 do |
||||
validator = insert(:validator_stability) |
||||
blocks_count = Enum.random(0..50) |
||||
|
||||
_ = |
||||
for _ <- 0..blocks_count do |
||||
insert(:block, miner_hash: validator.address_hash, miner: nil) |
||||
end |
||||
|
||||
{validator, blocks_count} |
||||
end |
||||
|> Enum.sort(&compare_default_sorting_for_asc/2) |
||||
|
||||
init_params = %{"sort" => "blocks_validated", "order" => "asc"} |
||||
request = get(conn, "/api/v2/validators/stability", init_params) |
||||
assert response = json_response(request, 200) |
||||
|
||||
request_2nd_page = |
||||
get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) |
||||
|
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
check_paginated_response(response, response_2nd_page, validators) |
||||
end |
||||
|
||||
test "sort by blocks_validated desc", %{conn: conn} do |
||||
validators = |
||||
for _ <- 0..50 do |
||||
validator = insert(:validator_stability) |
||||
blocks_count = Enum.random(0..50) |
||||
|
||||
_ = |
||||
for _ <- 0..blocks_count do |
||||
insert(:block, miner_hash: validator.address_hash, miner: nil) |
||||
end |
||||
|
||||
{validator, blocks_count} |
||||
end |
||||
|> Enum.sort(&compare_default_sorting_for_desc/2) |
||||
|
||||
init_params = %{"sort" => "blocks_validated", "order" => "desc"} |
||||
request = get(conn, "/api/v2/validators/stability", init_params) |
||||
assert response = json_response(request, 200) |
||||
|
||||
request_2nd_page = |
||||
get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) |
||||
|
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
check_paginated_response(response, response_2nd_page, validators) |
||||
end |
||||
|
||||
test "state_filter=probation", %{conn: conn} do |
||||
insert_list(51, :validator_stability, state: Enum.random([:active, :inactive])) |
||||
|
||||
validators = |
||||
insert_list(51, :validator_stability, state: :probation) |
||||
|> Enum.sort_by( |
||||
fn validator -> |
||||
{Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} |
||||
end, |
||||
:desc |
||||
) |
||||
|
||||
init_params = %{"state_filter" => "probation"} |
||||
|
||||
request = get(conn, "/api/v2/validators/stability", init_params) |
||||
assert response = json_response(request, 200) |
||||
|
||||
request_2nd_page = |
||||
get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) |
||||
|
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
check_paginated_response(response, response_2nd_page, validators) |
||||
end |
||||
end |
||||
|
||||
describe "/validators/stability/counters" do |
||||
test "get counters", %{conn: conn} do |
||||
_validator_active1 = |
||||
insert(:validator_stability, state: :active, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) |
||||
|
||||
_validator_active2 = insert(:validator_stability, state: :active) |
||||
_validator_active3 = insert(:validator_stability, state: :active) |
||||
|
||||
_validator_inactive1 = |
||||
insert(:validator_stability, state: :inactive, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) |
||||
|
||||
_validator_inactive2 = insert(:validator_stability, state: :inactive) |
||||
_validator_inactive3 = insert(:validator_stability, state: :inactive) |
||||
|
||||
_validator_probation1 = |
||||
insert(:validator_stability, state: :probation, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) |
||||
|
||||
_validator_probation2 = insert(:validator_stability, state: :probation) |
||||
_validator_probation3 = insert(:validator_stability, state: :probation) |
||||
|
||||
StabilityValidatorsCounters.consolidate() |
||||
:timer.sleep(500) |
||||
|
||||
percentage = (3 / 9 * 100) |> Float.floor(2) |
||||
request = get(conn, "/api/v2/validators/stability/counters") |
||||
|
||||
assert %{ |
||||
"active_validators_counter" => "3", |
||||
"active_validators_percentage" => ^percentage, |
||||
"new_validators_counter_24h" => "6", |
||||
"validators_counter" => "9" |
||||
} = json_response(request, 200) |
||||
end |
||||
end |
||||
end |
||||
|
||||
defp compare_item(%ValidatorStability{} = validator, json) do |
||||
assert Address.checksum(validator.address_hash) == json["address"]["hash"] |
||||
assert to_string(validator.state) == json["state"] |
||||
end |
||||
|
||||
defp compare_item({%ValidatorStability{} = validator, count}, json) do |
||||
assert json["blocks_validated_count"] == count + 1 |
||||
assert compare_item(validator, json) |
||||
end |
||||
|
||||
defp check_paginated_response(first_page_resp, second_page_resp, list) do |
||||
assert Enum.count(first_page_resp["items"]) == 50 |
||||
assert first_page_resp["next_page_params"] != nil |
||||
compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) |
||||
compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) |
||||
|
||||
assert Enum.count(second_page_resp["items"]) == 1 |
||||
assert second_page_resp["next_page_params"] == nil |
||||
compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) |
||||
end |
||||
|
||||
defp compare_default_sorting_for_asc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do |
||||
case { |
||||
compare(blocks_count_1, blocks_count_2), |
||||
compare( |
||||
Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state), |
||||
Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state) |
||||
), |
||||
compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes) |
||||
} do |
||||
{:lt, _, _} -> false |
||||
{:eq, :lt, _} -> false |
||||
{:eq, :eq, :lt} -> false |
||||
_ -> true |
||||
end |
||||
end |
||||
|
||||
defp compare_default_sorting_for_desc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do |
||||
case { |
||||
compare(blocks_count_1, blocks_count_2), |
||||
compare( |
||||
Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state), |
||||
Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state) |
||||
), |
||||
compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes) |
||||
} do |
||||
{:gt, _, _} -> false |
||||
{:eq, :lt, _} -> false |
||||
{:eq, :eq, :lt} -> false |
||||
_ -> true |
||||
end |
||||
end |
||||
|
||||
defp compare(a, b) do |
||||
cond do |
||||
a < b -> :lt |
||||
a > b -> :gt |
||||
true -> :eq |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,105 @@ |
||||
defmodule Explorer.Chain.Cache.StabilityValidatorsCounters do |
||||
@moduledoc """ |
||||
Counts and store counters of validators stability. |
||||
|
||||
It loads the count asynchronously and in a time interval of 30 minutes. |
||||
""" |
||||
|
||||
use GenServer |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.Stability.Validator, as: ValidatorStability |
||||
|
||||
@validators_counter_key "stability_validators_counter" |
||||
@new_validators_counter_key "new_stability_validators_counter" |
||||
@active_validators_counter_key "active_stability_validators_counter" |
||||
|
||||
# It is undesirable to automatically start the consolidation in all environments. |
||||
# Consider the test environment: if the consolidation initiates but does not |
||||
# finish before a test ends, that test will fail. This way, hundreds of |
||||
# tests were failing before disabling the consolidation and the scheduler in |
||||
# the test env. |
||||
config = Application.compile_env(:explorer, __MODULE__) |
||||
@enable_consolidation Keyword.get(config, :enable_consolidation) |
||||
|
||||
@update_interval_in_milliseconds Keyword.get(config, :update_interval_in_milliseconds) |
||||
|
||||
@doc """ |
||||
Starts a process to periodically update validators stability counters |
||||
""" |
||||
@spec start_link(term()) :: GenServer.on_start() |
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(_args) do |
||||
{:ok, %{consolidate?: @enable_consolidation}, {:continue, :ok}} |
||||
end |
||||
|
||||
defp schedule_next_consolidation do |
||||
Process.send_after(self(), :consolidate, @update_interval_in_milliseconds) |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:ok, %{consolidate?: true} = state) do |
||||
consolidate() |
||||
schedule_next_consolidation() |
||||
|
||||
{:noreply, state} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:ok, state) do |
||||
{:noreply, state} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:consolidate, state) do |
||||
consolidate() |
||||
schedule_next_consolidation() |
||||
|
||||
{:noreply, state} |
||||
end |
||||
|
||||
@doc """ |
||||
Fetches values for a stability validators counters from the `last_fetched_counters` table. |
||||
""" |
||||
@spec get_counters(Keyword.t()) :: map() |
||||
def get_counters(options) do |
||||
%{ |
||||
validators_counter: Chain.get_last_fetched_counter(@validators_counter_key, options), |
||||
new_validators_counter: Chain.get_last_fetched_counter(@new_validators_counter_key, options), |
||||
active_validators_counter: Chain.get_last_fetched_counter(@active_validators_counter_key, options) |
||||
} |
||||
end |
||||
|
||||
@doc """ |
||||
Consolidates the info by populating the `last_fetched_counters` table with the current database information. |
||||
""" |
||||
@spec consolidate() :: any() |
||||
def consolidate do |
||||
tasks = [ |
||||
Task.async(fn -> ValidatorStability.count_validators() end), |
||||
Task.async(fn -> ValidatorStability.count_new_validators() end), |
||||
Task.async(fn -> ValidatorStability.count_active_validators() end) |
||||
] |
||||
|
||||
[validators_counter, new_validators_counter, active_validators_counter] = Task.await_many(tasks, :infinity) |
||||
|
||||
Chain.upsert_last_fetched_counter(%{ |
||||
counter_type: @validators_counter_key, |
||||
value: validators_counter |
||||
}) |
||||
|
||||
Chain.upsert_last_fetched_counter(%{ |
||||
counter_type: @new_validators_counter_key, |
||||
value: new_validators_counter |
||||
}) |
||||
|
||||
Chain.upsert_last_fetched_counter(%{ |
||||
counter_type: @active_validators_counter_key, |
||||
value: active_validators_counter |
||||
}) |
||||
end |
||||
end |
@ -0,0 +1,288 @@ |
||||
defmodule Explorer.Chain.Stability.Validator do |
||||
@moduledoc """ |
||||
Stability validators |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{Address, Import} |
||||
alias Explorer.Chain.Hash.Address, as: HashAddress |
||||
alias Explorer.{Chain, Repo, SortingHelper} |
||||
alias Explorer.SmartContract.Reader |
||||
|
||||
require Logger |
||||
|
||||
@default_sorting [ |
||||
asc: :state, |
||||
asc: :address_hash |
||||
] |
||||
|
||||
@state_enum [active: 0, probation: 1, inactive: 2] |
||||
|
||||
@primary_key false |
||||
typed_schema "validators_stability" do |
||||
field(:address_hash, HashAddress, primary_key: true) |
||||
field(:state, Ecto.Enum, values: @state_enum) |
||||
field(:blocks_validated, :integer, virtual: true) |
||||
|
||||
has_one(:address, Address, foreign_key: :hash, references: :address_hash) |
||||
timestamps() |
||||
end |
||||
|
||||
@required_attrs ~w(address_hash)a |
||||
@optional_attrs ~w(state)a |
||||
def changeset(%__MODULE__{} = validator, attrs) do |
||||
validator |
||||
|> cast(attrs, @required_attrs ++ @optional_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> unique_constraint(:address_hash) |
||||
end |
||||
|
||||
@doc """ |
||||
Get validators list. |
||||
Keyword could contain: |
||||
- paging_options |
||||
- necessity_by_association |
||||
- sorting (supported by `Explorer.SortingHelper` module) |
||||
- state (one of `@state_enum`) |
||||
""" |
||||
@spec get_paginated_validators(keyword()) :: [t()] |
||||
def get_paginated_validators(options \\ []) do |
||||
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) |
||||
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||
sorting = Keyword.get(options, :sorting, []) |
||||
states = Keyword.get(options, :state, []) |
||||
|
||||
__MODULE__ |
||||
|> apply_filter_by_state(states) |
||||
|> select_merge([vs], %{ |
||||
blocks_validated: |
||||
fragment( |
||||
"SELECT count(*) FROM blocks WHERE miner_hash = ?", |
||||
vs.address_hash |
||||
) |
||||
}) |
||||
|> Chain.join_associations(necessity_by_association) |
||||
|> SortingHelper.apply_sorting(sorting, @default_sorting) |
||||
|> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) |
||||
|> Chain.select_repo(options).all() |
||||
end |
||||
|
||||
defp apply_filter_by_state(query, []), do: query |
||||
|
||||
defp apply_filter_by_state(query, states) do |
||||
query |
||||
|> where([vs], vs.state in ^states) |
||||
end |
||||
|
||||
@doc """ |
||||
Get all validators |
||||
""" |
||||
@spec get_all_validators(keyword()) :: [t()] |
||||
def get_all_validators(options \\ []) do |
||||
__MODULE__ |
||||
|> Chain.select_repo(options).all() |
||||
end |
||||
|
||||
@get_active_validator_list_abi %{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}], |
||||
"name" => "getActiveValidatorList", |
||||
"inputs" => [] |
||||
} |
||||
|
||||
@get_validator_list_abi %{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}], |
||||
"name" => "getValidatorList", |
||||
"inputs" => [] |
||||
} |
||||
|
||||
@get_validator_missing_blocks_abi %{ |
||||
"inputs" => [ |
||||
%{ |
||||
"internalType" => "address", |
||||
"name" => "validator", |
||||
"type" => "address" |
||||
} |
||||
], |
||||
"name" => "getValidatorMissingBlocks", |
||||
"outputs" => [ |
||||
%{ |
||||
"internalType" => "uint256", |
||||
"name" => "", |
||||
"type" => "uint256" |
||||
} |
||||
], |
||||
"stateMutability" => "view", |
||||
"type" => "function" |
||||
} |
||||
|
||||
@get_active_validator_list_method_id "a5aa7380" |
||||
@get_validator_list_method_id "e35c0f7d" |
||||
@get_validator_missing_blocks_method_id "41ee9a53" |
||||
|
||||
@stability_validator_controller_contract "0x0000000000000000000000000000000000000805" |
||||
|
||||
@doc """ |
||||
Do batch eth_call of `getValidatorList` and `getActiveValidatorList` methods to `@stability_validator_controller_contract`. |
||||
Returns a map with two lists: `active` and `all`, or nil if error. |
||||
""" |
||||
@spec fetch_validators_lists :: nil | %{active: list(binary()), all: list(binary())} |
||||
def fetch_validators_lists do |
||||
abi = [@get_active_validator_list_abi, @get_validator_list_abi] |
||||
params = %{@get_validator_list_method_id => [], @get_active_validator_list_method_id => []} |
||||
|
||||
case Reader.query_contract(@stability_validator_controller_contract, abi, params, false) do |
||||
%{ |
||||
@get_active_validator_list_method_id => {:ok, [active_validators_list]}, |
||||
@get_validator_list_method_id => {:ok, [validators_list]} |
||||
} -> |
||||
%{active: active_validators_list, all: validators_list} |
||||
|
||||
error -> |
||||
Logger.warn(fn -> ["Error on getting validator lists: #{inspect(error)}"] end) |
||||
nil |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Do batch eth_call of `getValidatorMissingBlocks` method to #{@stability_validator_controller_contract}. |
||||
Accept: list of validator address hashes |
||||
Returns a map: validator_address_hash => missing_blocks_number |
||||
""" |
||||
@spec fetch_missing_blocks_numbers(list(binary())) :: map() |
||||
def fetch_missing_blocks_numbers(validators_address_hashes) do |
||||
validators_address_hashes |
||||
|> Enum.map(&format_request_missing_blocks_number/1) |
||||
|> Reader.query_contracts([@get_validator_missing_blocks_abi]) |
||||
|> Enum.zip_reduce(validators_address_hashes, %{}, fn response, address_hash, acc -> |
||||
result = |
||||
case format_missing_blocks_result(response) do |
||||
{:error, message} -> |
||||
Logger.warn(fn -> ["Error on getValidatorMissingBlocks for #{validators_address_hashes}: #{message}"] end) |
||||
nil |
||||
|
||||
amount -> |
||||
amount |
||||
end |
||||
|
||||
Map.put(acc, address_hash, result) |
||||
end) |
||||
end |
||||
|
||||
defp format_missing_blocks_result({:ok, [amount]}) do |
||||
amount |
||||
end |
||||
|
||||
defp format_missing_blocks_result({:error, error_message}) do |
||||
{:error, error_message} |
||||
end |
||||
|
||||
defp format_request_missing_blocks_number(address_hash) do |
||||
%{ |
||||
contract_address: @stability_validator_controller_contract, |
||||
method_id: @get_validator_missing_blocks_method_id, |
||||
args: [address_hash] |
||||
} |
||||
end |
||||
|
||||
@doc """ |
||||
Convert missing block number to state |
||||
""" |
||||
@spec missing_block_number_to_state(integer()) :: atom() |
||||
def missing_block_number_to_state(integer) when integer > 0, do: :probation |
||||
def missing_block_number_to_state(integer) when integer == 0, do: :active |
||||
def missing_block_number_to_state(_), do: nil |
||||
|
||||
@doc """ |
||||
Delete validators by address hashes |
||||
""" |
||||
@spec delete_validators_by_address_hashes([binary() | HashAddress.t()]) :: {non_neg_integer(), nil | []} | :ignore |
||||
def delete_validators_by_address_hashes(list) when is_list(list) and length(list) > 0 do |
||||
__MODULE__ |
||||
|> where([vs], vs.address_hash in ^list) |
||||
|> Repo.delete_all() |
||||
end |
||||
|
||||
def delete_validators_by_address_hashes(_), do: :ignore |
||||
|
||||
@doc """ |
||||
Insert validators |
||||
""" |
||||
@spec insert_validators([map()]) :: {non_neg_integer(), nil | []} |
||||
def insert_validators(validators) do |
||||
Repo.insert_all(__MODULE__, validators, |
||||
on_conflict: {:replace_all_except, [:inserted_at]}, |
||||
conflict_target: [:address_hash] |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Append timestamps (:inserted_at, :updated_at) |
||||
""" |
||||
@spec append_timestamps(map()) :: map() |
||||
def append_timestamps(validator) do |
||||
Map.merge(validator, Import.timestamps()) |
||||
end |
||||
|
||||
@doc """ |
||||
Derive next page params from %Explorer.Chain.Stability.Validator{} |
||||
""" |
||||
@spec next_page_params(t()) :: map() |
||||
def next_page_params(%__MODULE__{state: state, address_hash: address_hash, blocks_validated: blocks_validated}) do |
||||
%{"state" => state, "address_hash" => address_hash, "blocks_validated" => blocks_validated} |
||||
end |
||||
|
||||
@doc """ |
||||
Returns state enum |
||||
""" |
||||
@spec state_enum() :: Keyword.t() |
||||
def state_enum, do: @state_enum |
||||
|
||||
@doc """ |
||||
Returns dynamic query for validated blocks count. Needed for SortingHelper |
||||
""" |
||||
@spec dynamic_validated_blocks() :: Ecto.Query.dynamic_expr() |
||||
def dynamic_validated_blocks do |
||||
dynamic( |
||||
[vs], |
||||
fragment( |
||||
"SELECT count(*) FROM blocks WHERE miner_hash = ?", |
||||
vs.address_hash |
||||
) |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns total count of validators. |
||||
""" |
||||
@spec count_validators() :: integer() |
||||
def count_validators do |
||||
Repo.aggregate(__MODULE__, :count, :address_hash) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns count of new validators (inserted withing last 24h). |
||||
""" |
||||
@spec count_new_validators() :: integer() |
||||
def count_new_validators do |
||||
__MODULE__ |
||||
|> where([vs], vs.inserted_at >= ago(1, "day")) |
||||
|> Repo.aggregate(:count, :address_hash) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns count of active validators. |
||||
""" |
||||
@spec count_active_validators() :: integer() |
||||
def count_active_validators do |
||||
__MODULE__ |
||||
|> where([vs], vs.state == :active) |
||||
|> Repo.aggregate(:count, :address_hash) |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
defmodule Explorer.Repo.Stability.Migrations.AddStabilityValidators do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:validators_stability, primary_key: false) do |
||||
add(:address_hash, :bytea, null: false, primary_key: true) |
||||
add(:state, :integer, default: 0) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
create_if_not_exists(index(:validators_stability, ["state ASC", "address_hash ASC"])) |
||||
end |
||||
end |
@ -0,0 +1,70 @@ |
||||
defmodule Indexer.Fetcher.Stability.Validator do |
||||
@moduledoc """ |
||||
GenServer responsible for updating the list of stability validators in the database. |
||||
""" |
||||
use GenServer |
||||
|
||||
alias Explorer.Chain.Hash.Address, as: AddressHash |
||||
alias Explorer.Chain.Stability.Validator, as: ValidatorStability |
||||
|
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__) |
||||
end |
||||
|
||||
def init(state) do |
||||
GenServer.cast(__MODULE__, :update_validators_list) |
||||
|
||||
{:ok, state} |
||||
end |
||||
|
||||
def handle_cast(:update_validators_list, state) do |
||||
validators_from_db = ValidatorStability.get_all_validators() |
||||
|
||||
case ValidatorStability.fetch_validators_lists() do |
||||
%{active: active_validator_addresses_list, all: validator_addresses_list} -> |
||||
validators_map = Enum.reduce(validator_addresses_list, %{}, fn address, map -> Map.put(map, address, true) end) |
||||
|
||||
active_validators_map = |
||||
Enum.reduce(active_validator_addresses_list, %{}, fn address, map -> Map.put(map, address, true) end) |
||||
|
||||
address_hashes_to_drop_from_db = |
||||
Enum.flat_map(validators_from_db, fn validator -> |
||||
(is_nil(validators_map[validator.address_hash.bytes]) && [validator.address_hash]) || [] |
||||
end) |
||||
|
||||
grouped = |
||||
Enum.group_by(validator_addresses_list, fn validator_address -> active_validators_map[validator_address] end) |
||||
|
||||
inactive = |
||||
Enum.map(grouped[nil] || [], fn address_hash -> |
||||
{:ok, address_hash} = AddressHash.load(address_hash) |
||||
|
||||
%{address_hash: address_hash, state: :inactive} |> ValidatorStability.append_timestamps() |
||||
end) |
||||
|
||||
validators_to_missing_blocks_numbers = ValidatorStability.fetch_missing_blocks_numbers(grouped[true] || []) |
||||
|
||||
active = |
||||
Enum.map(grouped[true] || [], fn address_hash_init -> |
||||
{:ok, address_hash} = AddressHash.load(address_hash_init) |
||||
|
||||
%{ |
||||
address_hash: address_hash, |
||||
state: |
||||
ValidatorStability.missing_block_number_to_state( |
||||
validators_to_missing_blocks_numbers[address_hash_init] |
||||
) |
||||
} |
||||
|> ValidatorStability.append_timestamps() |
||||
end) |
||||
|
||||
ValidatorStability.insert_validators(active ++ inactive) |
||||
ValidatorStability.delete_validators_by_address_hashes(address_hashes_to_drop_from_db) |
||||
|
||||
_ -> |
||||
nil |
||||
end |
||||
|
||||
{:noreply, state} |
||||
end |
||||
end |
@ -0,0 +1,270 @@ |
||||
defmodule Indexer.Fetcher.Stability.ValidatorTest do |
||||
use EthereumJSONRPC.Case |
||||
use Explorer.DataCase |
||||
|
||||
import Mox |
||||
|
||||
alias Explorer.Chain.Stability.Validator, as: ValidatorStability |
||||
alias EthereumJSONRPC.Encoder |
||||
|
||||
setup :verify_on_exit! |
||||
setup :set_mox_global |
||||
|
||||
@accepts_list_of_addresses %{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}], |
||||
"name" => "getActiveValidatorList", |
||||
"inputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}] |
||||
} |
||||
|
||||
@accepts_integer %{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [ |
||||
%{ |
||||
"internalType" => "uint256", |
||||
"name" => "", |
||||
"type" => "uint256" |
||||
} |
||||
], |
||||
"name" => "getActiveValidatorList", |
||||
"inputs" => [ |
||||
%{ |
||||
"internalType" => "uint256", |
||||
"name" => "", |
||||
"type" => "uint256" |
||||
} |
||||
] |
||||
} |
||||
|
||||
if Application.compile_env(:explorer, :chain_type) == "stability" do |
||||
describe "check update_validators_list" do |
||||
test "deletes absent validators" do |
||||
_validator = insert(:validator_stability) |
||||
_validator_active = insert(:validator_stability, state: :active) |
||||
_validator_inactive = insert(:validator_stability, state: :inactive) |
||||
_validator_probation = insert(:validator_stability, state: :probation) |
||||
|
||||
start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator}) |
||||
|
||||
EthereumJSONRPC.Mox |
||||
|> expect(:json_rpc, 1, fn |
||||
[ |
||||
%{ |
||||
id: id_1, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"] |
||||
}, |
||||
%{ |
||||
id: id_2, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"] |
||||
} |
||||
], |
||||
_ -> |
||||
<<"0x", _method_id::binary-size(8), result::binary>> = |
||||
[@accepts_list_of_addresses] |
||||
|> ABI.parse_specification() |
||||
|> Enum.at(0) |
||||
|> Encoder.encode_function_call([[]]) |
||||
|
||||
{:ok, |
||||
[ |
||||
%{ |
||||
id: id_1, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result |
||||
}, |
||||
%{ |
||||
id: id_2, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result |
||||
} |
||||
]} |
||||
end) |
||||
|
||||
:timer.sleep(100) |
||||
assert ValidatorStability.get_all_validators() == [] |
||||
end |
||||
|
||||
test "updates validators" do |
||||
validator_active1 = insert(:validator_stability, state: :active) |
||||
validator_active2 = insert(:validator_stability, state: :active) |
||||
_validator_active3 = insert(:validator_stability, state: :active) |
||||
|
||||
validator_inactive1 = insert(:validator_stability, state: :inactive) |
||||
validator_inactive2 = insert(:validator_stability, state: :inactive) |
||||
_validator_inactive3 = insert(:validator_stability, state: :inactive) |
||||
|
||||
validator_probation1 = insert(:validator_stability, state: :probation) |
||||
validator_probation2 = insert(:validator_stability, state: :probation) |
||||
_validator_probation3 = insert(:validator_stability, state: :probation) |
||||
|
||||
start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator}) |
||||
|
||||
EthereumJSONRPC.Mox |
||||
|> expect(:json_rpc, fn |
||||
[ |
||||
%{ |
||||
id: id_1, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"] |
||||
}, |
||||
%{ |
||||
id: id_2, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"] |
||||
} |
||||
], |
||||
_ -> |
||||
<<"0x", _method_id::binary-size(8), result_all::binary>> = |
||||
[@accepts_list_of_addresses] |
||||
|> ABI.parse_specification() |
||||
|> Enum.at(0) |
||||
|> Encoder.encode_function_call([ |
||||
[ |
||||
validator_active1.address_hash.bytes, |
||||
validator_active2.address_hash.bytes, |
||||
validator_inactive1.address_hash.bytes, |
||||
validator_inactive2.address_hash.bytes, |
||||
validator_probation1.address_hash.bytes, |
||||
validator_probation2.address_hash.bytes |
||||
] |
||||
]) |
||||
|
||||
<<"0x", _method_id::binary-size(8), result_active::binary>> = |
||||
[@accepts_list_of_addresses] |
||||
|> ABI.parse_specification() |
||||
|> Enum.at(0) |
||||
|> Encoder.encode_function_call([ |
||||
[ |
||||
validator_active1.address_hash.bytes, |
||||
validator_inactive1.address_hash.bytes, |
||||
validator_probation1.address_hash.bytes |
||||
] |
||||
]) |
||||
|
||||
{:ok, |
||||
[ |
||||
%{ |
||||
id: id_1, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result_active |
||||
}, |
||||
%{ |
||||
id: id_2, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result_all |
||||
} |
||||
]} |
||||
end) |
||||
|
||||
"0x" <> address_1 = to_string(validator_active1.address_hash) |
||||
"0x" <> address_2 = to_string(validator_inactive1.address_hash) |
||||
"0x" <> address_3 = to_string(validator_probation1.address_hash) |
||||
|
||||
EthereumJSONRPC.Mox |
||||
|> expect(:json_rpc, fn |
||||
[ |
||||
%{ |
||||
id: id_1, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [ |
||||
%{ |
||||
data: "0x41ee9a53000000000000000000000000" <> ^address_1, |
||||
to: "0x0000000000000000000000000000000000000805" |
||||
}, |
||||
"latest" |
||||
] |
||||
}, |
||||
%{ |
||||
id: id_2, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [ |
||||
%{ |
||||
data: "0x41ee9a53000000000000000000000000" <> ^address_2, |
||||
to: "0x0000000000000000000000000000000000000805" |
||||
}, |
||||
"latest" |
||||
] |
||||
}, |
||||
%{ |
||||
id: id_3, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: [ |
||||
%{ |
||||
data: "0x41ee9a53000000000000000000000000" <> ^address_3, |
||||
to: "0x0000000000000000000000000000000000000805" |
||||
}, |
||||
"latest" |
||||
] |
||||
} |
||||
], |
||||
_ -> |
||||
<<"0x", _method_id::binary-size(8), result_1::binary>> = |
||||
[@accepts_integer] |
||||
|> ABI.parse_specification() |
||||
|> Enum.at(0) |
||||
|> Encoder.encode_function_call([10]) |
||||
|
||||
<<"0x", _method_id::binary-size(8), result_2::binary>> = |
||||
[@accepts_integer] |
||||
|> ABI.parse_specification() |
||||
|> Enum.at(0) |
||||
|> Encoder.encode_function_call([1]) |
||||
|
||||
<<"0x", _method_id::binary-size(8), result_3::binary>> = |
||||
[@accepts_integer] |
||||
|> ABI.parse_specification() |
||||
|> Enum.at(0) |
||||
|> Encoder.encode_function_call([0]) |
||||
|
||||
{:ok, |
||||
[ |
||||
%{ |
||||
id: id_1, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result_1 |
||||
}, |
||||
%{ |
||||
id: id_2, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result_2 |
||||
}, |
||||
%{ |
||||
id: id_3, |
||||
jsonrpc: "2.0", |
||||
result: "0x" <> result_3 |
||||
} |
||||
]} |
||||
end) |
||||
|
||||
:timer.sleep(100) |
||||
validators = ValidatorStability.get_all_validators() |
||||
|
||||
assert Enum.count(validators) == 6 |
||||
|
||||
map = |
||||
Enum.reduce(validators, %{}, fn validator, map -> Map.put(map, validator.address_hash.bytes, validator) end) |
||||
|
||||
assert %ValidatorStability{state: :inactive} = map[validator_active2.address_hash.bytes] |
||||
assert %ValidatorStability{state: :inactive} = map[validator_inactive2.address_hash.bytes] |
||||
assert %ValidatorStability{state: :inactive} = map[validator_probation2.address_hash.bytes] |
||||
|
||||
assert %ValidatorStability{state: :probation} = map[validator_active1.address_hash.bytes] |
||||
assert %ValidatorStability{state: :probation} = map[validator_inactive1.address_hash.bytes] |
||||
assert %ValidatorStability{state: :active} = map[validator_probation1.address_hash.bytes] |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue