diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad4cad924..9d787745b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - [#9631](https://github.com/blockscout/blockscout/pull/9631) - Initial support of zksync chain type +- [#9532](https://github.com/blockscout/blockscout/pull/9532) - Add last output root size counter - [#9490](https://github.com/blockscout/blockscout/pull/9490) - Add blob transaction counter and filter in block view - [#9486](https://github.com/blockscout/blockscout/pull/9486) - Massive blocks fetcher - [#9473](https://github.com/blockscout/blockscout/pull/9473) - Add user_op interpretation diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 6ba3b57e7b..f4bb234f24 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -3,11 +3,10 @@ defmodule BlockScoutWeb.API.V2.StatsController do alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.Chain.MarketHistoryChartController - alias EthereumJSONRPC.Variant alias Explorer.{Chain, Market} alias Explorer.Chain.Address.Counters alias Explorer.Chain.Cache.Block, as: BlockCache - alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage, RootstockLockedBTC} + alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage} alias Explorer.Chain.Cache.Transaction, as: TransactionCache alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats @@ -78,7 +77,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do "tvl" => exchange_rate.tvl_usd, "network_utilization_percentage" => network_utilization_percentage() } - |> add_rootstock_locked_btc() + |> add_chain_type_fields() |> backward_compatibility(conn) ) end @@ -148,15 +147,6 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end - defp add_rootstock_locked_btc(stats) do - with "rsk" <- Variant.get(), - rootstock_locked_btc when not is_nil(rootstock_locked_btc) <- RootstockLockedBTC.get_locked_value() do - stats |> Map.put("rootstock_locked_btc", rootstock_locked_btc) - else - _ -> stats - end - end - defp backward_compatibility(response, conn) do case Conn.get_req_header(conn, "updated-gas-oracle") do ["true"] -> @@ -170,4 +160,28 @@ defmodule BlockScoutWeb.API.V2.StatsController do end) end end + + case Application.compile_env(:explorer, :chain_type) do + "rsk" -> + defp add_chain_type_fields(response) do + alias Explorer.Chain.Cache.RootstockLockedBTC + + case RootstockLockedBTC.get_locked_value() do + rootstock_locked_btc when not is_nil(rootstock_locked_btc) -> + response |> Map.put("rootstock_locked_btc", rootstock_locked_btc) + + _ -> + response + end + end + + "optimism" -> + defp add_chain_type_fields(response) do + import Explorer.Counters.LastOutputRootSizeCounter, only: [fetch: 1] + response |> Map.put("last_output_root_size", fetch(@api_true)) + end + + _ -> + defp add_chain_type_fields(response), do: response + end end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 27e34b729c..c54b712937 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -18,6 +18,7 @@ config :explorer, Explorer.Chain.Transaction.History.Historian, enabled: false config :explorer, Explorer.Market.History.Historian, enabled: false config :explorer, Explorer.Counters.AddressesCounter, enabled: false, enable_consolidation: false +config :explorer, Explorer.Counters.LastOutputRootSizeCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Chain.Cache.ContractsCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Chain.Cache.NewContractsCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Chain.Cache.VerifiedContractsCounter, enabled: false, enable_consolidation: false diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index f7faba77ac..b5d33647d2 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -118,6 +118,7 @@ defmodule Explorer.Application do configure(Explorer.Counters.BlockBurntFeeCounter), configure(Explorer.Counters.BlockPriorityFeeCounter), configure(Explorer.Counters.AverageBlockTime), + configure(Explorer.Counters.LastOutputRootSizeCounter), configure(Explorer.Validator.MetadataProcessor), configure(Explorer.Tags.AddressTag.Cataloger), configure(MinMissingBlockNumber), diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 6e58566709..fb9ee353dc 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -2120,7 +2120,11 @@ defmodule Explorer.Chain do select: last_fetched_counter.value ) - select_repo(options).one(query) || Decimal.new(0) + if options[:nullable] do + select_repo(options).one(query) + else + select_repo(options).one(query) || Decimal.new(0) + end end defp block_status({number, timestamp}) do diff --git a/apps/explorer/lib/explorer/counters/last_output_root_size_counter.ex b/apps/explorer/lib/explorer/counters/last_output_root_size_counter.ex new file mode 100644 index 0000000000..c910dbe6da --- /dev/null +++ b/apps/explorer/lib/explorer/counters/last_output_root_size_counter.ex @@ -0,0 +1,112 @@ +defmodule Explorer.Counters.LastOutputRootSizeCounter do + @moduledoc """ + Caches number of transactions in last output root. + + It loads the count asynchronously and in a time interval of :cache_period (default to 5 minutes). + """ + + use GenServer + + import Ecto.Query + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Optimism.OutputRoot + alias Explorer.Chain.Transaction + + @counter_type "last_output_root_size_count" + + @doc """ + Starts a process to periodically update the counter. + """ + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, 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, cache_interval()) + 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 the value for a `#{@counter_type}` counter type from the `last_fetched_counters` table. + """ + def fetch(options) do + Chain.get_last_fetched_counter(@counter_type, options |> Keyword.put_new(:nullable, true)) + end + + @doc """ + Consolidates the info by populating the `last_fetched_counters` table with the current database information. + """ + def consolidate do + output_root_query = + from(root in OutputRoot, + select: {root.l2_block_number}, + order_by: [desc: root.l2_output_index], + limit: 2 + ) + + count = + case output_root_query |> Repo.all() do + [{last_block_number}, {prev_block_number}] -> + query = + from(transaction in Transaction, + where: + not is_nil(transaction.block_hash) and transaction.block_number > ^prev_block_number and + transaction.block_number <= ^last_block_number, + select: count(transaction.hash) + ) + + Repo.one!(query, timeout: :infinity) + + _ -> + nil + end + + Chain.upsert_last_fetched_counter(%{ + counter_type: @counter_type, + value: count + }) + end + + @doc """ + Returns a boolean that indicates whether consolidation is enabled + + In order to choose whether or not to enable the scheduler and the initial + consolidation, change the following Explorer config: + + `config :explorer, #{__MODULE__}, enable_consolidation: true` + + to: + + `config :explorer, #{__MODULE__}, enable_consolidation: false` + """ + def enable_consolidation?, do: Application.get_env(:explorer, __MODULE__)[:enable_consolidation] + + defp cache_interval, do: Application.get_env(:explorer, __MODULE__)[:cache_period] +end diff --git a/apps/explorer/test/explorer/counters/last_output_root_size_counter_test.exs b/apps/explorer/test/explorer/counters/last_output_root_size_counter_test.exs new file mode 100644 index 0000000000..4f00da7cb2 --- /dev/null +++ b/apps/explorer/test/explorer/counters/last_output_root_size_counter_test.exs @@ -0,0 +1,47 @@ +defmodule Explorer.Counters.LastOutputRootSizeCounterTest do + use Explorer.DataCase + + alias Explorer.Counters.LastOutputRootSizeCounter + + if Application.compile_env(:explorer, :chain_type) == "optimism" do + test "populates the cache with the number of transactions in last output root" do + first_block = insert(:block) + + insert(:op_output_root, l2_block_number: first_block.number) + + second_block = insert(:block, number: first_block.number + 10) + insert(:op_output_root, l2_block_number: second_block.number) + + insert(:transaction) |> with_block(first_block) + insert(:transaction) |> with_block(second_block) + insert(:transaction) |> with_block(second_block) + + start_supervised!(LastOutputRootSizeCounter) + LastOutputRootSizeCounter.consolidate() + + assert LastOutputRootSizeCounter.fetch([]) == Decimal.new("2") + end + + test "does not count transactions that are not in output root yet" do + first_block = insert(:block) + + insert(:op_output_root, l2_block_number: first_block.number) + + second_block = insert(:block, number: first_block.number + 10) + insert(:op_output_root, l2_block_number: second_block.number) + + insert(:transaction) |> with_block(first_block) + insert(:transaction) |> with_block(second_block) + insert(:transaction) |> with_block(second_block) + + third_block = insert(:block, number: second_block.number + 1) + insert(:transaction) |> with_block(third_block) + insert(:transaction) |> with_block(third_block) + + start_supervised!(LastOutputRootSizeCounter) + LastOutputRootSizeCounter.consolidate() + + assert LastOutputRootSizeCounter.fetch([]) == Decimal.new("2") + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 3730205068..0c9a98e43a 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -48,6 +48,8 @@ defmodule Explorer.Factory do Withdrawal } + alias Explorer.Chain.Optimism.OutputRoot + alias Explorer.SmartContract.Helper alias Explorer.Tags.{AddressTag, AddressToTag} alias Explorer.Market.MarketHistory @@ -1116,6 +1118,34 @@ defmodule Explorer.Factory do } end + def op_output_root_factory do + %OutputRoot{ + l2_output_index: op_output_root_l2_output_index(), + l2_block_number: insert(:block) |> Map.get(:number), + l1_transaction_hash: transaction_hash(), + l1_timestamp: DateTime.utc_now(), + l1_block_number: op_output_root_l1_block_number(), + output_root: op_output_root_hash() + } + end + + defp op_output_root_l2_output_index do + sequence("op_output_root_l2_output_index", & &1) + end + + defp op_output_root_l1_block_number do + sequence("op_output_root_l1_block_number", & &1) + end + + defp op_output_root_hash do + {:ok, hash} = + "op_output_root_hash" + |> sequence(& &1) + |> Hash.Full.cast() + + hash + end + def random_bool, do: Enum.random([true, false]) def validator_stability_factory do diff --git a/config/runtime.exs b/config/runtime.exs index 5cac7d87b6..96e2938e44 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -291,6 +291,11 @@ config :explorer, Explorer.Counters.AddressTokenUsdSum, config :explorer, Explorer.Counters.AddressTokenTransfersCounter, cache_period: ConfigHelper.parse_time_env_var("CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD", "1h") +config :explorer, Explorer.Counters.LastOutputRootSizeCounter, + enabled: true, + enable_consolidation: true, + cache_period: ConfigHelper.parse_time_env_var("CACHE_OPTIMISM_LAST_OUTPUT_ROOT_SIZE_COUNTER_PERIOD", "5m") + config :explorer, Explorer.ExchangeRates, store: :ets, enabled: !disable_exchange_rates?,