From 546b732ac13630e5e224af44d3e41e6e3a0c1293 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:17:26 +0300 Subject: [PATCH] Add stability validators (#9390) * Add stability validators * Process review comments * Fix tests --- CHANGELOG.md | 1 + .../lib/block_scout_web/api_router.ex | 9 + .../lib/block_scout_web/chain.ex | 28 ++ .../api/v2/validator_controller.ex | 83 +++++ .../lib/block_scout_web/paging_helper.ex | 40 ++- .../views/api/v2/validator_view.ex | 17 ++ .../api/v2/validator_controller_test.exs | 205 +++++++++++++ apps/block_scout_web/test/test_helper.exs | 1 + apps/explorer/config/config.exs | 5 + apps/explorer/config/dev.exs | 2 + apps/explorer/config/prod.exs | 4 + apps/explorer/config/test.exs | 4 +- apps/explorer/lib/explorer/application.ex | 14 +- .../cache/stability_validators_counters.ex | 105 +++++++ .../lib/explorer/chain/stability/validator.ex | 288 ++++++++++++++++++ apps/explorer/lib/explorer/repo.ex | 10 + ...0240203091010_add_stability_validators.exs | 14 + apps/explorer/test/support/factory.ex | 10 + apps/explorer/test/test_helper.exs | 1 + .../lib/indexer/block/realtime/fetcher.ex | 30 +- .../indexer/fetcher/stability/validator.ex | 70 +++++ apps/indexer/lib/indexer/supervisor.ex | 16 +- .../fetcher/stability/validator_test.exs | 270 ++++++++++++++++ config/config_helper.exs | 1 + config/runtime/dev.exs | 9 + config/runtime/prod.exs | 8 + 26 files changed, 1231 insertions(+), 14 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs create mode 100644 apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex create mode 100644 apps/explorer/lib/explorer/chain/stability/validator.ex create mode 100644 apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs create mode 100644 apps/indexer/lib/indexer/fetcher/stability/validator.ex create mode 100644 apps/indexer/test/indexer/fetcher/stability/validator_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d783d81e4..dfeaa7ecac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [#9490](https://github.com/blockscout/blockscout/pull/9490) - Add blob transaction counter and filter in block view - [#9461](https://github.com/blockscout/blockscout/pull/9461) - Fetch blocks without internal transactions backwards - [#9460](https://github.com/blockscout/blockscout/pull/9460) - Optimism chain type +- [#9390](https://github.com/blockscout/blockscout/pull/9390) - Add stability validators - [#8702](https://github.com/blockscout/blockscout/pull/8702) - Add OP withdrawal status to transaction page in API - [#7200](https://github.com/blockscout/blockscout/pull/7200) - Add Optimism BedRock Deposits to the main page in API - [#6980](https://github.com/blockscout/blockscout/pull/6980) - Add Optimism BedRock support (Txn Batches, Output Roots, Deposits, Withdrawals) diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index 403d78d3a4..39a9475bba 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -369,6 +369,15 @@ defmodule BlockScoutWeb.ApiRouter do get("/:blob_hash_param", V2.BlobController, :blob) end end + + scope "/validators" do + if Application.compile_env(:explorer, :chain_type) == "stability" do + scope "/stability" do + get("/", V2.ValidatorController, :stability_validators_list) + get("/counters", V2.ValidatorController, :stability_validators_counters) + end + end + end end scope "/v1", as: :api_v1 do diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 8db363d135..d0e110218e 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -17,7 +17,9 @@ defmodule BlockScoutWeb.Chain do import Explorer.Helper, only: [parse_integer: 1] + alias BlockScoutWeb.PagingHelper alias Ecto.Association.NotLoaded + alias Explorer.Chain.UserOperation alias Explorer.Account.{TagAddress, TagTransaction, WatchlistAddress} alias Explorer.Chain.Beacon.Reader, as: BeaconReader alias Explorer.Chain.Block.Reward @@ -459,6 +461,25 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: {token_contract_address_hash, token_type}}] end + # Clause for `Explorer.Chain.Stability.Validator`, + # returned by `BlockScoutWeb.API.V2.ValidatorController.stability_validators_list/2` (`/api/v2/validators/stability`) + def paging_options(%{ + "state" => state, + "address_hash" => address_hash_string, + "blocks_validated" => blocks_validated_string + }) do + [ + paging_options: %{ + @default_paging_options + | key: %{ + address_hash: parse_address_hash(address_hash_string), + blocks_validated: parse_integer(blocks_validated_string), + state: if(state in PagingHelper.allowed_stability_validators_states(), do: state) + } + } + ] + end + def paging_options(_params), do: [paging_options: @default_paging_options] def put_key_value_to_paging_options([paging_options: paging_options], key, value) do @@ -541,6 +562,13 @@ defmodule BlockScoutWeb.Chain do end end + defp parse_address_hash(address_hash_string) do + case Hash.Address.cast(address_hash_string) do + {:ok, address_hash} -> address_hash + _ -> nil + end + end + defp paging_params({%Address{hash: hash, fetched_coin_balance: fetched_coin_balance}, _}) do %{"hash" => hash, "fetched_coin_balance" => Decimal.to_string(fetched_coin_balance.value)} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex new file mode 100644 index 0000000000..4c78835028 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 1a054b091e..72914f1f5d 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.PagingHelper do Helper for fetching filters and other url query parameters """ import Explorer.Chain, only: [string_to_transaction_hash: 1] + alias Explorer.Chain.Stability.Validator, as: ValidatorStability alias Explorer.Chain.Transaction alias Explorer.{Helper, PagingOptions, SortingHelper} @@ -34,6 +35,9 @@ defmodule BlockScoutWeb.PagingHelper do @allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155"] @allowed_nft_token_type_labels ["ERC-721", "ERC-1155"] @allowed_chain_id [1, 56, 99] + @allowed_stability_validators_states ["active", "probation", "inactive"] + + def allowed_stability_validators_states, do: @allowed_stability_validators_states def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do with {block_number, ""} <- Integer.parse(block_number_string), @@ -57,6 +61,13 @@ defmodule BlockScoutWeb.PagingHelper do def paging_options(_params, _filter), do: [paging_options: @default_paging_options] + @spec stability_validators_state_options(map()) :: [{:state, list()}, ...] + def stability_validators_state_options(%{"state_filter" => state}) do + [state: filters_to_list(state, @allowed_stability_validators_states, :downcase)] + end + + def stability_validators_state_options(_), do: [state: []] + @spec token_transfers_types_options(map()) :: [{:token_type, list}] def token_transfers_types_options(%{"type" => filters}) do [ @@ -78,7 +89,9 @@ defmodule BlockScoutWeb.PagingHelper do def nft_token_types_options(_), do: [token_type: []] - defp filters_to_list(filters, allowed), do: filters |> String.upcase() |> parse_filter(allowed) + defp filters_to_list(filters, allowed, variant \\ :upcase) + defp filters_to_list(filters, allowed, :downcase), do: filters |> String.downcase() |> parse_filter(allowed) + defp filters_to_list(filters, allowed, :upcase), do: filters |> String.upcase() |> parse_filter(allowed) # sobelow_skip ["DOS.StringToAtom"] def filter_options(%{"filter" => filter}, fallback) do @@ -188,7 +201,8 @@ defmodule BlockScoutWeb.PagingHelper do "filter", "q", "sort", - "order" + "order", + "state_filter" ]) end @@ -267,4 +281,26 @@ defmodule BlockScoutWeb.PagingHelper do do: [{:dynamic, :fee, :desc_nulls_last, Transaction.dynamic_fee()}] defp do_address_transaction_sorting(_, _), do: [] + + @spec validators_stability_sorting(%{required(String.t()) => String.t()}) :: [ + {:sorting, SortingHelper.sorting_params()} + ] + def validators_stability_sorting(%{"sort" => sort_field, "order" => order}) do + [sorting: do_validators_stability_sorting(sort_field, order)] + end + + def validators_stability_sorting(_), do: [] + + defp do_validators_stability_sorting("state", "asc"), do: [asc_nulls_first: :state] + defp do_validators_stability_sorting("state", "desc"), do: [desc_nulls_last: :state] + defp do_validators_stability_sorting("address_hash", "asc"), do: [asc_nulls_first: :address_hash] + defp do_validators_stability_sorting("address_hash", "desc"), do: [desc_nulls_last: :address_hash] + + defp do_validators_stability_sorting("blocks_validated", "asc"), + do: [{:dynamic, :blocks_validated, :asc_nulls_first, ValidatorStability.dynamic_validated_blocks()}] + + defp do_validators_stability_sorting("blocks_validated", "desc"), + do: [{:dynamic, :blocks_validated, :desc_nulls_last, ValidatorStability.dynamic_validated_blocks()}] + + defp do_validators_stability_sorting(_, _), do: [] end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex new file mode 100644 index 0000000000..e5b719557b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs new file mode 100644 index 0000000000..9981c830ca --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index c14efe9d63..0c0ba46a2f 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -32,6 +32,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Beacon, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :manual) diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index dcc12a69bd..2c1e69d061 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -76,6 +76,11 @@ config :explorer, Explorer.Chain.Cache.WithdrawalsSum, enable_consolidation: true, update_interval_in_milliseconds: update_interval_in_milliseconds_default +config :explorer, Explorer.Chain.Cache.StabilityValidatorsCounters, + enabled: true, + enable_consolidation: true, + update_interval_in_milliseconds: update_interval_in_milliseconds_default + config :explorer, Explorer.Chain.Cache.TransactionActionTokensData, enabled: true config :explorer, Explorer.Chain.Cache.TransactionActionUniswapPools, enabled: true diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index 85e792bd27..9932940356 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -32,6 +32,8 @@ config :explorer, Explorer.Repo.BridgedTokens, timeout: :timer.seconds(80) config :explorer, Explorer.Repo.Filecoin, timeout: :timer.seconds(80) +config :explorer, Explorer.Repo.Stability, timeout: :timer.seconds(80) + config :explorer, Explorer.Tracer, env: "dev", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index 2b74469e83..4e4519adf9 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -52,6 +52,10 @@ config :explorer, Explorer.Repo.Filecoin, prepare: :unnamed, timeout: :timer.seconds(60) +config :explorer, Explorer.Repo.Stability, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Tracer, env: "production", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index e815f20829..d4130b4272 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -52,7 +52,8 @@ for repo <- [ Explorer.Repo.Shibarium, Explorer.Repo.Suave, Explorer.Repo.BridgedTokens, - Explorer.Repo.Filecoin + Explorer.Repo.Filecoin, + Explorer.Repo.Stability ] do config :explorer, repo, database: "explorer_test", @@ -84,3 +85,4 @@ config :explorer, Explorer.ExchangeRates.Source.TransactionAndLog, config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: false config :explorer, Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand, enabled: false +config :explorer, Explorer.Tags.AddressTag.Cataloger, enabled: false diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index aac514bed7..35e749c247 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -132,7 +132,8 @@ defmodule Explorer.Application do configure(Explorer.Migrator.AddressTokenBalanceTokenType), configure(Explorer.Migrator.SanitizeMissingBlockRanges), configure(Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers), - configure(Explorer.Migrator.TokenTransferTokenType) + configure(Explorer.Migrator.TokenTransferTokenType), + configure_chain_type_dependent_process(Explorer.Chain.Cache.StabilityValidatorsCounters, "stability") ] |> List.flatten() @@ -150,7 +151,8 @@ defmodule Explorer.Application do Explorer.Repo.Shibarium, Explorer.Repo.Suave, Explorer.Repo.BridgedTokens, - Explorer.Repo.Filecoin + Explorer.Repo.Filecoin, + Explorer.Repo.Stability ] else [] @@ -177,6 +179,14 @@ defmodule Explorer.Application do end end + defp configure_chain_type_dependent_process(process, chain_type) do + if Application.get_env(:explorer, :chain_type) == chain_type do + process + else + [] + end + end + defp sc_microservice_configure(process) do if Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:eth_bytecode_db?] do process diff --git a/apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex b/apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex new file mode 100644 index 0000000000..1034465848 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/stability/validator.ex b/apps/explorer/lib/explorer/chain/stability/validator.ex new file mode 100644 index 0000000000..309d95814b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/stability/validator.ex @@ -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 diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex index 2e457379d8..9dbbb9f9d4 100644 --- a/apps/explorer/lib/explorer/repo.ex +++ b/apps/explorer/lib/explorer/repo.ex @@ -240,4 +240,14 @@ defmodule Explorer.Repo do ConfigHelper.init_repo_module(__MODULE__, opts) end end + + defmodule Stability do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres + + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end end diff --git a/apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs b/apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs new file mode 100644 index 0000000000..67e5580fef --- /dev/null +++ b/apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs @@ -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 diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 7474c1c40e..3730205068 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -23,6 +23,7 @@ defmodule Explorer.Factory do alias Explorer.Admin.Administrator alias Explorer.Chain.Beacon.{Blob, BlobTransaction} alias Explorer.Chain.Block.{EmissionReward, Range, Reward} + alias Explorer.Chain.Stability.Validator, as: ValidatorStability alias Explorer.Chain.{ Address, @@ -1116,4 +1117,13 @@ defmodule Explorer.Factory do end def random_bool, do: Enum.random([true, false]) + + def validator_stability_factory do + address = insert(:address) + + %ValidatorStability{ + address_hash: address.hash, + state: Enum.random(0..2) + } + end end diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index 882fb7a26c..90d21435e5 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -21,6 +21,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Beacon, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :auto) Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source) Mox.defmock(Explorer.Market.History.Source.Price.TestSource, for: Explorer.Market.History.Source.Price) diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 61e86cf122..7f1219ef2c 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -130,14 +130,18 @@ defmodule Indexer.Block.Realtime.Fetcher do new_previous_number = case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do {:ok, number} when is_nil(previous_number) or number != previous_number -> - if abnormal_gap?(number, previous_number) do - new_number = max(number, previous_number) - start_fetch_and_import(new_number, block_fetcher, previous_number) - new_number - else - start_fetch_and_import(number, block_fetcher, previous_number) - number - end + number = + if abnormal_gap?(number, previous_number) do + new_number = max(number, previous_number) + start_fetch_and_import(new_number, block_fetcher, previous_number) + new_number + else + start_fetch_and_import(number, block_fetcher, previous_number) + number + end + + fetch_validators_async() + number _ -> previous_number @@ -158,6 +162,16 @@ defmodule Indexer.Block.Realtime.Fetcher do {:noreply, state} end + if Application.compile_env(:explorer, :chain_type) == "stability" do + defp fetch_validators_async do + GenServer.cast(Indexer.Fetcher.Stability.Validator, :update_validators_list) + end + else + defp fetch_validators_async do + :ignore + end + end + defp subscribe_to_new_heads(%__MODULE__{subscription: nil} = state, subscribe_named_arguments) when is_list(subscribe_named_arguments) do case EthereumJSONRPC.subscribe("newHeads", subscribe_named_arguments) do diff --git a/apps/indexer/lib/indexer/fetcher/stability/validator.ex b/apps/indexer/lib/indexer/fetcher/stability/validator.ex new file mode 100644 index 0000000000..70851e8d8a --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/stability/validator.ex @@ -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 diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index b1018baa39..6d585a2f74 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -20,6 +20,7 @@ defmodule Indexer.Supervisor do alias Indexer.Block.Realtime, as: BlockRealtime alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime + alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.TokenInstance.LegacySanitize, as: TokenInstanceLegacySanitize alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry @@ -203,7 +204,10 @@ defmodule Indexer.Supervisor do ] |> List.flatten() - all_fetchers = maybe_add_bridged_tokens_fetchers(basic_fetchers) + all_fetchers = + basic_fetchers + |> maybe_add_bridged_tokens_fetchers() + |> add_chain_type_dependent_fetchers() Supervisor.init( all_fetchers, @@ -228,6 +232,16 @@ defmodule Indexer.Supervisor do end end + defp add_chain_type_dependent_fetchers(fetchers) do + case Application.get_env(:explorer, :chain_type) do + "stability" -> + [{ValidatorStability, []} | fetchers] + + _ -> + fetchers + end + end + defp configure(process, opts) do if Application.get_env(:indexer, process)[:enabled] do [{process, opts}] diff --git a/apps/indexer/test/indexer/fetcher/stability/validator_test.exs b/apps/indexer/test/indexer/fetcher/stability/validator_test.exs new file mode 100644 index 0000000000..8aae393a34 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/stability/validator_test.exs @@ -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 diff --git a/config/config_helper.exs b/config/config_helper.exs index a5c0a2e67f..5c2eab9dd5 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -17,6 +17,7 @@ defmodule ConfigHelper do "shibarium" -> base_repos ++ [Explorer.Repo.Shibarium] "suave" -> base_repos ++ [Explorer.Repo.Suave] "filecoin" -> base_repos ++ [Explorer.Repo.Filecoin] + "stability" -> base_repos ++ [Explorer.Repo.Stability] _ -> base_repos end diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index 12b0d3bde8..d889a4bbd6 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -147,6 +147,15 @@ config :explorer, Explorer.Repo.Filecoin, url: System.get_env("DATABASE_URL"), pool_size: 1 +# Configures Stability database +config :explorer, Explorer.Repo.Stability, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1 + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/dev") diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index abf8c2137d..3fc64ea80b 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -113,6 +113,14 @@ config :explorer, Explorer.Repo.Filecoin, pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() +# Configures Stability database +config :explorer, Explorer.Repo.Stability, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/prod")