<%= @address.contract_code %>
- <%= gettext "Contracts that self destruct in their constructors have no contract code published and cannot be verified." %>
+<%= gettext "Displaying the init data provided of the creating transaction." %>
+<%= transaction_init %>
+ <%= contract_code %>
+ #
\n # eveem.org 6 Feb 2019
\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875
\n #
\n # Let's make the world open source
\n #
\n #
\n # I failed with these:
\n # - unknowne77c646d(?)
\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)
\n # All the rest is below.
\n #
\n
\n
\n # Storage definitions and getters
\n
\n def storage:
\n allowance is uint256 => uint256 # mask(256, 0) at storage #2
\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4
\n
\n def allowance(address _owner, address _spender) payable: 64
\n return allowance[_owner_spender(320 - 1))]
\n
\n
\n #
\n # Regular functions - see Tutorial for understanding quirks of the code
\n #
\n
\n
\n # folder failed in this function - may be terribly long, sorry
\n def unknownc47d033b(?) payable: not cd[4]:
\n revert
\n else:
\n mem[0]cd[4]
\n mem[32] = 4
\n mem[96] = bool(stor4[cd[4])])
\n return bool(stor4[cd[4])])
\n
\n def _fallback() payable: # default function
\n revert
\n
\n
\n"
+ " #
\n # eveem.org 6 Feb 2019
\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875
\n #
\n # Let's make the world open source
\n #
\n #
\n # I failed with these:
\n # - unknowne77c646d(?)
\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)
\n # All the rest is below.
\n #
\n
\n
\n # Storage definitions and getters
\n
\n def storage:
\n allowance is uint256 => uint256 # mask(256, 0) at storage #2
\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4
\n
\n def allowance(address _owner, address _spender) payable:
\n require (calldata.size - 4) >= 64
\n return allowance[sha3(((320 - 1) and (320 - 1) and _owner), 1), ((320 - 1) and _spender and (320 - 1))]
\n
\n
\n #
\n # Regular functions - see Tutorial for understanding quirks of the code
\n #
\n
\n
\n # folder failed in this function - may be terribly long, sorry
\n def unknownc47d033b(?) payable:
\n if (calldata.size - 4) < 32:
\n revert
\n else:
\n if not (320 - 1) or not cd[4]:
\n revert
\n else:
\n mem[0] = (320 - 1) and (320 - 1) and cd[4]
\n mem[32] = 4
\n mem[96] = bool(stor4[((320 - 1) and (320 - 1) and cd[4])])
\n return bool(stor4[((320 - 1) and (320 - 1) and cd[4])])
\n
\n def _fallback() payable: # default function
\n revert
\n
\n
\n"
end
test "adds style span to every line" do
@@ -72,6 +72,28 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do
assert AddressDecompiledContractView.highlight_decompiled_code(code) ==
" #
\n # eveem.org 6 Feb 2019
\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875
\n #
\n # Let's make the world open source
\n #
\n
\n
\n"
end
+
+ test "does not remove bold text" do
+ code = """
+ [38;5;8m#
+ # Eveem.org 26 Apr 2019
+ # Decompiled source of [0m[92m0x06012c8cf97bead5deae237070f9587f8e7a266d[0m[38;5;8m
+ #
+ # Let's make the world open source
+ # [0m
+
+ [95mconst [0mname = [1m'CryptoKitties'[0m
+ [95mconst [0msymbol = [1m'CK'[0m
+ [95mconst [0mGEN0_STARTING_PRICE = [1m10^16[0m
+ [95mconst [0mGEN0_AUCTION_DURATION = [1m(24 * 3600)[0m
+ [95mconst [0mGEN0_CREATION_LIMIT = [1m45000[0m
+ [95mconst [0mPROMO_CREATION_LIMIT = [1m5000[0m
+
+ """
+
+ assert AddressDecompiledContractView.highlight_decompiled_code(code) ==
+ "#
\n# Eveem.org 26 Apr 2019
\n# Decompiled source of 0x06012c8cf97bead5deae237070f9587f8e7a266d
\n#
\n# Let's make the world open source
\n#
\n
\nconst name = 'CryptoKitties'
\nconst symbol = 'CK'
\nconst GEN0_STARTING_PRICE = 10^16
\nconst GEN0_AUCTION_DURATION = (24 * 3600)
\nconst GEN0_CREATION_LIMIT = 45000
\nconst PROMO_CREATION_LIMIT = 5000
\n
\n
\n
\n"
+ end
end
describe "sort_contracts_by_version/1" do
diff --git a/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs
index 5e3302ceec..5787159b14 100644
--- a/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs
@@ -62,16 +62,32 @@ defmodule BlockScoutWeb.LayoutViewTest do
end
describe "release_link/1" do
- test "use the version when there is no release_link env configured for it" do
+ test "set empty string if no blockscout version configured" do
+ Application.put_env(:block_scout_web, :blockscout_version, nil)
+
+ assert LayoutView.release_link(nil) == ""
+ end
+
+ test "set empty string if blockscout version is empty string" do
+ Application.put_env(:block_scout_web, :blockscout_version, "")
+
+ assert LayoutView.release_link("") == ""
+ end
+
+ test "use the default value when there is no release_link env configured for it" do
Application.put_env(:block_scout_web, :release_link, nil)
- assert LayoutView.release_link("1.3.4") == "1.3.4"
+ assert LayoutView.release_link("v1.3.4-beta") ==
+ {:safe,
+ ~s(v1.3.4-beta)}
end
- test "use the version when empty release_link env configured for it" do
+ test "use the default value when empty release_link env configured for it" do
Application.put_env(:block_scout_web, :release_link, "")
- assert LayoutView.release_link("1.3.4") == "1.3.4"
+ assert LayoutView.release_link("v1.3.4-beta") ==
+ {:safe,
+ ~s(v1.3.4-beta)}
end
test "use the enviroment release link when it's configured" do
@@ -81,9 +97,9 @@ defmodule BlockScoutWeb.LayoutViewTest do
"https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta"
)
- assert LayoutView.release_link("1.3.4") ==
+ assert LayoutView.release_link("v1.3.4-beta") ==
{:safe,
- ~s(1.3.4)}
+ ~s(v1.3.4-beta)}
end
end
diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs
index e6eb5c4813..0ba1a960c7 100644
--- a/apps/explorer/config/config.exs
+++ b/apps/explorer/config/config.exs
@@ -13,6 +13,8 @@ config :explorer,
config :explorer, Explorer.Counters.AverageBlockTime, enabled: true
+config :explorer, Explorer.Chain.BlockNumberCache, enabled: true
+
config :explorer, Explorer.ExchangeRates.Source.CoinMarketCap,
pages: String.to_integer(System.get_env("COINMARKETCAP_PAGES") || "10")
@@ -58,6 +60,14 @@ config :explorer, Explorer.Staking.PoolsReader,
validators_contract_address: System.get_env("POS_VALIDATORS_CONTRACT"),
staking_contract_address: System.get_env("POS_STAKING_CONTRACT")
+if System.get_env("POS_STAKING_CONTRACT") do
+ config :explorer, Explorer.Staking.EpochCounter,
+ enabled: true,
+ staking_contract_address: System.get_env("POS_STAKING_CONTRACT")
+else
+ config :explorer, Explorer.Staking.EpochCounter, enabled: false
+end
+
if System.get_env("SUPPLY_MODULE") == "TokenBridge" do
config :explorer, supply: Explorer.Chain.Supply.TokenBridge
end
diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs
index 0a717d8b27..f27fc3b350 100644
--- a/apps/explorer/config/test.exs
+++ b/apps/explorer/config/test.exs
@@ -13,6 +13,8 @@ config :explorer, Explorer.Repo,
config :explorer, Explorer.ExchangeRates, enabled: false, store: :ets
+config :explorer, Explorer.Chain.BlockNumberCache, enabled: false
+
config :explorer, Explorer.KnownTokens, enabled: false, store: :ets
config :explorer, Explorer.Counters.AverageBlockTime, enabled: false
diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex
index 417b1eaa8f..d886dd8a0b 100644
--- a/apps/explorer/lib/explorer/application.ex
+++ b/apps/explorer/lib/explorer/application.ex
@@ -51,7 +51,8 @@ defmodule Explorer.Application do
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.AddressesWithBalanceCounter),
configure(Explorer.Counters.AverageBlockTime),
- configure(Explorer.Validator.MetadataProcessor)
+ configure(Explorer.Validator.MetadataProcessor),
+ configure(Explorer.Staking.EpochCounter)
]
|> List.flatten()
end
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 713d449d72..0977df1dd2 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -10,6 +10,7 @@ defmodule Explorer.Chain do
limit: 2,
order_by: 2,
order_by: 3,
+ offset: 2,
preload: 2,
select: 2,
subquery: 1,
@@ -279,6 +280,38 @@ defmodule Explorer.Chain do
|> Enum.take(paging_options.page_size)
end
+ @spec address_to_logs(Address.t(), [paging_options]) :: [
+ Log.t()
+ ]
+ def address_to_logs(
+ %Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash},
+ options \\ []
+ )
+ when is_list(options) do
+ paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50}
+
+ {block_number, transaction_index, log_index} = paging_options.key || {BlockNumberCache.max_number(), 0, 0}
+
+ query =
+ from(log in Log,
+ inner_join: transaction in assoc(log, :transaction),
+ order_by: [desc: transaction.block_number, desc: transaction.index],
+ preload: [:transaction],
+ where:
+ log.address_hash == ^address_hash and
+ (transaction.block_number < ^block_number or
+ (transaction.block_number == ^block_number and transaction.index > ^transaction_index) or
+ (transaction.block_number == ^block_number and transaction.index == ^transaction_index and
+ log.index > ^log_index)),
+ limit: ^paging_options.page_size,
+ select: log
+ )
+
+ query
+ |> Repo.all()
+ |> Enum.take(paging_options.page_size)
+ end
+
@doc """
Finds all `t:Explorer.Chain.Transaction.t/0`s given the address_hash and the token contract
address hash.
@@ -2565,7 +2598,7 @@ defmodule Explorer.Chain do
join: duplicate in subquery(query),
on: duplicate.nonce == pending.nonce,
on: duplicate.from_address_hash == pending.from_address_hash,
- where: pending.hash in ^hashes
+ where: pending.hash in ^hashes and is_nil(pending.block_hash)
)
Repo.update_all(transactions_to_update, [set: [error: "dropped/replaced", status: :error]], timeout: timeout)
@@ -2802,6 +2835,7 @@ defmodule Explorer.Chain do
on: smart_contract.address_hash == address.hash,
where: not is_nil(address.contract_code),
where: is_nil(smart_contract.address_hash),
+ where: address.contract_code != <<>>,
preload: [{:smart_contract, smart_contract}, :decompiled_smart_contracts],
order_by: [asc: address.inserted_at],
limit: ^limit,
@@ -2811,6 +2845,19 @@ defmodule Explorer.Chain do
Repo.all(query)
end
+ def list_empty_contracts(limit, offset) do
+ query =
+ from(address in Address,
+ where: address.contract_code == <<>>,
+ preload: [:smart_contract, :decompiled_smart_contracts],
+ order_by: [asc: address.inserted_at],
+ limit: ^limit,
+ offset: ^offset
+ )
+
+ Repo.all(query)
+ end
+
def list_not_decompiled_contracts(limit, offset) do
query =
from(
@@ -2820,6 +2867,7 @@ defmodule Explorer.Chain do
"NOT EXISTS (SELECT 1 FROM decompiled_smart_contracts WHERE decompiled_smart_contracts.address_hash = ?)",
address.hash
),
+ where: address.contract_code != <<>>,
left_join: smart_contract in SmartContract,
on: smart_contract.address_hash == address.hash,
left_join: decompiled_smart_contract in DecompiledSmartContract,
@@ -2856,6 +2904,75 @@ defmodule Explorer.Chain do
value
end
+ @doc "Get staking pools from the DB"
+ @spec staking_pools(filter :: :validator | :active | :inactive, options :: PagingOptions.t()) :: [map()]
+ def staking_pools(filter, %PagingOptions{page_size: page_size, page_number: page_number} \\ @default_paging_options) do
+ off = page_size * (page_number - 1)
+
+ Address.Name
+ |> staking_pool_filter(filter)
+ |> limit(^page_size)
+ |> offset(^off)
+ |> Repo.all()
+ end
+
+ @doc "Get count of staking pools from the DB"
+ @spec staking_pools_count(filter :: :validator | :active | :inactive) :: integer
+ def staking_pools_count(filter) do
+ Address.Name
+ |> staking_pool_filter(filter)
+ |> Repo.aggregate(:count, :address_hash)
+ end
+
+ defp staking_pool_filter(query, :validator) do
+ where(
+ query,
+ [address],
+ fragment(
+ """
+ (?->>'is_active')::boolean = true and
+ (?->>'deleted')::boolean is not true and
+ (?->>'is_validator')::boolean = true
+ """,
+ address.metadata,
+ address.metadata,
+ address.metadata
+ )
+ )
+ end
+
+ defp staking_pool_filter(query, :active) do
+ where(
+ query,
+ [address],
+ fragment(
+ """
+ (?->>'is_active')::boolean = true and
+ (?->>'deleted')::boolean is not true
+ """,
+ address.metadata,
+ address.metadata
+ )
+ )
+ end
+
+ defp staking_pool_filter(query, :inactive) do
+ where(
+ query,
+ [address],
+ fragment(
+ """
+ (?->>'is_active')::boolean = false and
+ (?->>'deleted')::boolean is not true
+ """,
+ address.metadata,
+ address.metadata
+ )
+ )
+ end
+
+ defp staking_pool_filter(query, _), do: query
+
defp with_decompiled_code_flag(query, hash) do
has_decompiled_code_query =
from(decompiled_contract in DecompiledSmartContract,
diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex
index 58d187d9d8..9cf86b570b 100644
--- a/apps/explorer/lib/explorer/chain/block.ex
+++ b/apps/explorer/lib/explorer/chain/block.ex
@@ -10,7 +10,7 @@ defmodule Explorer.Chain.Block do
alias Explorer.Chain.{Address, Gas, Hash, Transaction}
alias Explorer.Chain.Block.{Reward, SecondDegreeRelation}
- @optional_attrs ~w(internal_transactions_indexed_at size)a
+ @optional_attrs ~w(internal_transactions_indexed_at size refetch_needed)a
@required_attrs ~w(consensus difficulty gas_limit gas_used hash miner_hash nonce number parent_hash timestamp total_difficulty)a
@@ -63,7 +63,8 @@ defmodule Explorer.Chain.Block do
timestamp: DateTime.t(),
total_difficulty: difficulty(),
transactions: %Ecto.Association.NotLoaded{} | [Transaction.t()],
- internal_transactions_indexed_at: DateTime.t()
+ internal_transactions_indexed_at: DateTime.t(),
+ refetch_needed: boolean()
}
@primary_key {:hash, Hash.Full, autogenerate: false}
@@ -78,6 +79,7 @@ defmodule Explorer.Chain.Block do
field(:timestamp, :utc_datetime_usec)
field(:total_difficulty, :decimal)
field(:internal_transactions_indexed_at, :utc_datetime_usec)
+ field(:refetch_needed, :boolean)
timestamps()
diff --git a/apps/explorer/lib/explorer/chain/block_number_cache.ex b/apps/explorer/lib/explorer/chain/block_number_cache.ex
index 28d8d83e9c..2f335c7a2c 100644
--- a/apps/explorer/lib/explorer/chain/block_number_cache.ex
+++ b/apps/explorer/lib/explorer/chain/block_number_cache.ex
@@ -6,13 +6,10 @@ defmodule Explorer.Chain.BlockNumberCache do
alias Explorer.Chain
@tab :block_number_cache
- # 30 minutes
- @cache_period 1_000 * 60 * 30
@key "min_max"
- @opts_key "opts"
- @spec setup(Keyword.t()) :: :ok
- def setup(opts \\ []) do
+ @spec setup() :: :ok
+ def setup do
if :ets.whereis(@tab) == :undefined do
:ets.new(@tab, [
:set,
@@ -22,7 +19,6 @@ defmodule Explorer.Chain.BlockNumberCache do
])
end
- setup_opts(opts)
update_cache()
:ok
@@ -41,15 +37,11 @@ defmodule Explorer.Chain.BlockNumberCache do
end
defp value(type) do
- initial_cache = {_min, _max, old_current_time} = cached_values()
-
- {min, max, _current_time} =
- if current_time() - old_current_time > cache_period() do
- update_cache()
-
+ {min, max} =
+ if Application.get_env(:explorer, __MODULE__)[:enabled] do
cached_values()
else
- initial_cache
+ min_and_max_from_db()
end
case type do
@@ -59,18 +51,29 @@ defmodule Explorer.Chain.BlockNumberCache do
end
end
- defp update_cache do
- current_time = current_time()
- {min, max} = min_and_max_from_db()
- tuple = {min, max, current_time}
+ @spec update(non_neg_integer()) :: boolean()
+ def update(number) do
+ {old_min, old_max} = cached_values()
- :ets.insert(@tab, {@key, tuple})
+ cond do
+ number > old_max ->
+ tuple = {old_min, number}
+ :ets.insert(@tab, {@key, tuple})
+
+ number < old_min ->
+ tuple = {number, old_max}
+ :ets.insert(@tab, {@key, tuple})
+
+ true ->
+ false
+ end
end
- defp setup_opts(opts) do
- cache_period = opts[:cache_period] || @cache_period
+ defp update_cache do
+ {min, max} = min_and_max_from_db()
+ tuple = {min, max}
- :ets.insert(@tab, {@opts_key, cache_period})
+ :ets.insert(@tab, {@key, tuple})
end
defp cached_values do
@@ -79,22 +82,10 @@ defmodule Explorer.Chain.BlockNumberCache do
cached_values
end
- defp cache_period do
- [{_, cache_period}] = :ets.lookup(@tab, @opts_key)
-
- cache_period
- end
-
defp min_and_max_from_db do
Chain.fetch_min_and_max_block_numbers()
rescue
_e ->
{0, 0}
end
-
- defp current_time do
- utc_now = DateTime.utc_now()
-
- DateTime.to_unix(utc_now, :millisecond)
- end
end
diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
index aaf5d7242e..b21c0c4441 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
@@ -41,6 +41,9 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
|> Map.put(:timestamps, timestamps)
multi
+ |> Multi.run(:mark_as_deleted, fn repo, _ ->
+ mark_as_deleted(repo, changes_list, insert_options)
+ end)
|> Multi.run(:insert_staking_pools, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
@@ -49,6 +52,32 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
@impl Import.Runner
def timeout, do: @timeout
+ defp mark_as_deleted(repo, changes_list, %{timeout: timeout}) when is_list(changes_list) do
+ addresses = Enum.map(changes_list, & &1.address_hash)
+
+ query =
+ from(
+ address_name in Address.Name,
+ where:
+ address_name.address_hash not in ^addresses and
+ fragment("(?->>'is_pool')::boolean = true", address_name.metadata),
+ update: [
+ set: [
+ metadata: fragment("? || '{\"deleted\": true}'::jsonb", address_name.metadata)
+ ]
+ ]
+ )
+
+ try do
+ {_, result} = repo.update_all(query, [], timeout: timeout)
+
+ {:ok, result}
+ rescue
+ postgrex_error in Postgrex.Error ->
+ {:error, %{exception: postgrex_error}}
+ end
+ end
+
@spec insert(Repo.t(), [map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
@@ -62,7 +91,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
{:ok, _} =
Import.insert_changes_list(
repo,
- changes_list,
+ stakes_ratio(changes_list),
conflict_target: {:unsafe_fragment, "(address_hash) where \"primary\" = true"},
on_conflict: on_conflict,
for: Address.Name,
@@ -85,4 +114,20 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
]
)
end
+
+ # Calculates staked ratio for each pool
+ defp stakes_ratio(pools) do
+ active_pools = Enum.filter(pools, & &1.metadata[:is_active])
+
+ stakes_total =
+ Enum.reduce(pools, 0, fn pool, acc ->
+ acc + pool.metadata[:staked_amount]
+ end)
+
+ Enum.map(active_pools, fn pool ->
+ staked_ratio = if stakes_total > 0, do: pool.metadata[:staked_amount] / stakes_total, else: 0
+
+ put_in(pool, [:metadata, :staked_ratio], staked_ratio)
+ end)
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex
index 4f1d2d6fe5..1a9fcc497d 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex
@@ -8,7 +8,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
import Ecto.Query, only: [from: 2]
alias Ecto.{Multi, Repo}
- alias Explorer.Chain.{Data, Hash, Import, Transaction}
+ alias Explorer.Chain.{Block, Data, Hash, Import, Transaction}
alias Explorer.Chain.Import.Runner.TokenTransfers
@behaviour Import.Runner
@@ -42,9 +42,13 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
|> Map.put(:timestamps, timestamps)
|> Map.put(:token_transfer_transaction_hash_set, token_transfer_transaction_hash_set(options))
- Multi.run(multi, :transactions, fn repo, _ ->
+ multi
+ |> Multi.run(:transactions, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
+ |> Multi.run(:recollated_transactions, fn repo, %{transactions: transactions} ->
+ discard_blocks_for_recollated_transactions(repo, transactions, insert_options)
+ end)
end
@impl Import.Runner
@@ -87,7 +91,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
on_conflict: on_conflict,
for: Transaction,
returning:
- ~w(block_number index hash internal_transactions_indexed_at block_hash nonce from_address_hash created_contract_address_hash)a,
+ ~w(block_number index hash internal_transactions_indexed_at block_hash old_block_hash nonce from_address_hash created_contract_address_hash)a,
timeout: timeout,
timestamps: timestamps
)
@@ -99,6 +103,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
update: [
set: [
block_hash: fragment("EXCLUDED.block_hash"),
+ old_block_hash: transaction.block_hash,
block_number: fragment("EXCLUDED.block_number"),
created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"),
cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"),
@@ -179,4 +184,43 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
end
defp put_internal_transactions_indexed_at?(_, _), do: false
+
+ defp discard_blocks_for_recollated_transactions(repo, transactions, %{
+ timeout: timeout,
+ timestamps: %{updated_at: updated_at}
+ })
+ when is_list(transactions) do
+ ordered_block_hashes =
+ transactions
+ |> Enum.filter(fn %{block_hash: block_hash, old_block_hash: old_block_hash} ->
+ not is_nil(old_block_hash) and block_hash != old_block_hash
+ end)
+ |> MapSet.new(& &1.old_block_hash)
+ |> Enum.sort()
+
+ if Enum.empty?(ordered_block_hashes) do
+ {:ok, []}
+ else
+ query =
+ from(
+ block in Block,
+ where: block.hash in ^ordered_block_hashes,
+ update: [
+ set: [
+ consensus: false,
+ updated_at: ^updated_at
+ ]
+ ]
+ )
+
+ try do
+ {_, result} = repo.update_all(query, [], timeout: timeout)
+
+ {:ok, result}
+ rescue
+ postgrex_error in Postgrex.Error ->
+ {:error, %{exception: postgrex_error, block_hashes: ordered_block_hashes}}
+ end
+ end
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/result.ex b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex
index 5b4e3102fc..c4c680e7d4 100644
--- a/apps/explorer/lib/explorer/chain/internal_transaction/result.ex
+++ b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex
@@ -19,7 +19,7 @@ defmodule Explorer.Chain.InternalTransaction.Result do
{key, to_string(hash)}
end
- defp entry_to_raw({"code", _} = entry), do: entry
+ defp entry_to_raw({"code", code}), do: {"code", Data.to_string(code)}
defp entry_to_raw({key, decimal}) when key in ~w(gasUsed) do
integer =
diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex
index 1cf9c8f116..de99198181 100644
--- a/apps/explorer/lib/explorer/chain/transaction.ex
+++ b/apps/explorer/lib/explorer/chain/transaction.ex
@@ -205,6 +205,11 @@ defmodule Explorer.Chain.Transaction do
field(:v, :decimal)
field(:value, Wei)
+ # A transient field for deriving old block hash during transaction upserts.
+ # Used to force refetch of a block in case a transaction is re-collated
+ # in a different block. See: https://github.com/poanetwork/blockscout/issues/1911
+ field(:old_block_hash, Hash.Full)
+
timestamps()
belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full)
diff --git a/apps/explorer/lib/explorer/paging_options.ex b/apps/explorer/lib/explorer/paging_options.ex
index 1bac2dc71a..0828a0afad 100644
--- a/apps/explorer/lib/explorer/paging_options.ex
+++ b/apps/explorer/lib/explorer/paging_options.ex
@@ -4,10 +4,11 @@ defmodule Explorer.PagingOptions do
number and index.
"""
- @type t :: %__MODULE__{key: key, page_size: page_size}
+ @type t :: %__MODULE__{key: key, page_size: page_size, page_number: page_number}
@typep key :: any()
@typep page_size :: non_neg_integer()
+ @typep page_number :: pos_integer()
- defstruct [:key, :page_size]
+ defstruct [:key, :page_size, page_number: 1]
end
diff --git a/apps/explorer/lib/explorer/staking/epoch_counter.ex b/apps/explorer/lib/explorer/staking/epoch_counter.ex
new file mode 100644
index 0000000000..c35ec1a6c9
--- /dev/null
+++ b/apps/explorer/lib/explorer/staking/epoch_counter.ex
@@ -0,0 +1,124 @@
+defmodule Explorer.Staking.EpochCounter do
+ @moduledoc """
+ Fetches current staking epoch number and the epoch end block number.
+ It subscribes to handle new blocks and conclude whether the epoch is over.
+ """
+
+ use GenServer
+
+ alias Explorer.Chain.Events.Subscriber
+ alias Explorer.SmartContract.Reader
+
+ @table_name __MODULE__
+ @epoch_key "epoch_num"
+ @epoch_end_key "epoch_end_block"
+
+ @doc "Current staking epoch number"
+ def epoch_number do
+ if :ets.info(@table_name) != :undefined do
+ case :ets.lookup(@table_name, @epoch_key) do
+ [{_, epoch_num}] ->
+ epoch_num
+
+ _ ->
+ 0
+ end
+ end
+ end
+
+ @doc "Block number on which will start new epoch"
+ def epoch_end_block do
+ if :ets.info(@table_name) != :undefined do
+ case :ets.lookup(@table_name, @epoch_end_key) do
+ [{_, epoch_end}] ->
+ epoch_end
+
+ _ ->
+ 0
+ end
+ end
+ end
+
+ def start_link([]) do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def init([]) do
+ :ets.new(@table_name, [
+ :set,
+ :named_table,
+ :public,
+ write_concurrency: true
+ ])
+
+ Subscriber.to(:blocks, :realtime)
+ {:ok, [], {:continue, :epoch_info}}
+ end
+
+ def handle_continue(:epoch_info, state) do
+ fetch_epoch_info()
+ {:noreply, state}
+ end
+
+ @doc "Handles new blocks and decides to fetch new epoch info"
+ def handle_info({:chain_event, :blocks, :realtime, blocks}, state) do
+ new_block_number =
+ blocks
+ |> Enum.map(&Map.get(&1, :number, 0))
+ |> Enum.max(fn -> 0 end)
+
+ case :ets.lookup(@table_name, @epoch_end_key) do
+ [] ->
+ fetch_epoch_info()
+
+ [{_, epoch_end_block}] when epoch_end_block < new_block_number ->
+ fetch_epoch_info()
+
+ _ ->
+ :ok
+ end
+
+ {:noreply, state}
+ end
+
+ defp fetch_epoch_info do
+ with data <- get_epoch_info(),
+ {:ok, [epoch_num]} <- data["stakingEpoch"],
+ {:ok, [epoch_end_block]} <- data["stakingEpochEndBlock"] do
+ :ets.insert(@table_name, {@epoch_key, epoch_num})
+ :ets.insert(@table_name, {@epoch_end_key, epoch_end_block})
+ end
+ end
+
+ defp get_epoch_info do
+ contract_abi = abi("staking.json")
+
+ functions = ["stakingEpoch", "stakingEpochEndBlock"]
+
+ functions
+ |> Enum.map(fn function ->
+ %{
+ contract_address: staking_address(),
+ function_name: function,
+ args: []
+ }
+ end)
+ |> Reader.query_contracts(contract_abi)
+ |> Enum.zip(functions)
+ |> Enum.into(%{}, fn {response, function} ->
+ {function, response}
+ end)
+ end
+
+ defp staking_address do
+ Application.get_env(:explorer, __MODULE__, [])[:staking_contract_address]
+ end
+
+ # sobelow_skip ["Traversal"]
+ defp abi(file_name) do
+ :explorer
+ |> Application.app_dir("priv/contracts_abi/pos/#{file_name}")
+ |> File.read!()
+ |> Jason.decode!()
+ end
+end
diff --git a/apps/explorer/lib/explorer/staking/pools_reader.ex b/apps/explorer/lib/explorer/staking/pools_reader.ex
index de03ff10b5..608fea3863 100644
--- a/apps/explorer/lib/explorer/staking/pools_reader.ex
+++ b/apps/explorer/lib/explorer/staking/pools_reader.ex
@@ -29,6 +29,7 @@ defmodule Explorer.Staking.PoolsReader do
{:ok, [delegator_addresses]} <- data["poolDelegators"],
delegators_count = Enum.count(delegator_addresses),
{:ok, [staked_amount]} <- data["stakeAmountTotalMinusOrderedWithdraw"],
+ {:ok, [self_staked_amount]} <- data["stakeAmountMinusOrderedWithdraw"],
{:ok, [is_validator]} <- data["isValidator"],
{:ok, [was_validator_count]} <- data["validatorCounter"],
{:ok, [is_banned]} <- data["isValidatorBanned"],
@@ -42,6 +43,7 @@ defmodule Explorer.Staking.PoolsReader do
is_active: is_active,
delegators_count: delegators_count,
staked_amount: staked_amount,
+ self_staked_amount: self_staked_amount,
is_validator: is_validator,
was_validator_count: was_validator_count,
is_banned: is_banned,
@@ -77,14 +79,15 @@ defmodule Explorer.Staking.PoolsReader do
contract_abi = abi("staking.json") ++ abi("validators.json")
methods = [
- {:staking, "isPoolActive", staking_address},
- {:staking, "poolDelegators", staking_address},
- {:staking, "stakeAmountTotalMinusOrderedWithdraw", staking_address},
- {:validators, "isValidator", mining_address},
- {:validators, "validatorCounter", mining_address},
- {:validators, "isValidatorBanned", mining_address},
- {:validators, "bannedUntil", mining_address},
- {:validators, "banCounter", mining_address}
+ {:staking, "isPoolActive", [staking_address]},
+ {:staking, "poolDelegators", [staking_address]},
+ {:staking, "stakeAmountTotalMinusOrderedWithdraw", [staking_address]},
+ {:staking, "stakeAmountMinusOrderedWithdraw", [staking_address, staking_address]},
+ {:validators, "isValidator", [mining_address]},
+ {:validators, "validatorCounter", [mining_address]},
+ {:validators, "isValidatorBanned", [mining_address]},
+ {:validators, "bannedUntil", [mining_address]},
+ {:validators, "banCounter", [mining_address]}
]
methods
@@ -96,11 +99,11 @@ defmodule Explorer.Staking.PoolsReader do
end)
end
- defp format_request({contract_name, function_name, param}) do
+ defp format_request({contract_name, function_name, params}) do
%{
contract_address: contract(contract_name),
function_name: function_name,
- args: [param]
+ args: params
}
end
diff --git a/apps/explorer/priv/contracts_abi/pos/staking.json b/apps/explorer/priv/contracts_abi/pos/staking.json
index 7bcbcfb18c..33f773ea61 100644
--- a/apps/explorer/priv/contracts_abi/pos/staking.json
+++ b/apps/explorer/priv/contracts_abi/pos/staking.json
@@ -1,36 +1,17 @@
[
{
"constant": true,
- "inputs": [],
- "name": "STAKE_UNIT",
- "outputs": [
+ "inputs": [
{
- "name": "",
- "type": "uint256"
- }
- ],
- "payable": false,
- "stateMutability": "view",
- "type": "function"
- },
- {
- "constant": true,
- "inputs": [],
- "name": "MAX_DELEGATORS_PER_POOL",
- "outputs": [
+ "name": "_poolStakingAddress",
+ "type": "address"
+ },
{
- "name": "",
- "type": "uint256"
+ "name": "_delegator",
+ "type": "address"
}
],
- "payable": false,
- "stateMutability": "view",
- "type": "function"
- },
- {
- "constant": true,
- "inputs": [],
- "name": "MAX_CANDIDATES",
+ "name": "poolDelegatorIndex",
"outputs": [
{
"name": "",
@@ -42,154 +23,55 @@
"type": "function"
},
{
- "anonymous": false,
- "inputs": [
- {
- "indexed": true,
- "name": "fromPoolStakingAddress",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "staker",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "stakingEpoch",
- "type": "uint256"
- },
- {
- "indexed": false,
- "name": "amount",
- "type": "uint256"
- }
- ],
- "name": "Claimed",
- "type": "event"
- },
- {
- "anonymous": false,
+ "constant": true,
"inputs": [
{
- "indexed": true,
- "name": "toPoolStakingAddress",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "staker",
+ "name": "_poolStakingAddress",
"type": "address"
- },
- {
- "indexed": true,
- "name": "stakingEpoch",
- "type": "uint256"
- },
- {
- "indexed": false,
- "name": "amount",
- "type": "uint256"
}
],
- "name": "Staked",
- "type": "event"
- },
- {
- "anonymous": false,
- "inputs": [
- {
- "indexed": false,
- "name": "fromPoolStakingAddress",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "toPoolStakingAddress",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "staker",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "stakingEpoch",
- "type": "uint256"
- },
+ "name": "stakeAmountTotalMinusOrderedWithdraw",
+ "outputs": [
{
- "indexed": false,
- "name": "amount",
+ "name": "",
"type": "uint256"
}
],
- "name": "StakeMoved",
- "type": "event"
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
},
{
- "anonymous": false,
+ "constant": false,
"inputs": [
{
- "indexed": true,
- "name": "fromPoolStakingAddress",
- "type": "address"
- },
- {
- "indexed": true,
- "name": "staker",
+ "name": "_erc20TokenContract",
"type": "address"
- },
- {
- "indexed": true,
- "name": "stakingEpoch",
- "type": "uint256"
- },
- {
- "indexed": false,
- "name": "amount",
- "type": "int256"
}
],
- "name": "WithdrawalOrdered",
- "type": "event"
+ "name": "setErc20TokenContract",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
},
{
- "anonymous": false,
+ "constant": false,
"inputs": [
{
- "indexed": true,
- "name": "fromPoolStakingAddress",
+ "name": "_fromPoolStakingAddress",
"type": "address"
},
{
- "indexed": true,
- "name": "staker",
+ "name": "_toPoolStakingAddress",
"type": "address"
},
{
- "indexed": true,
- "name": "stakingEpoch",
- "type": "uint256"
- },
- {
- "indexed": false,
- "name": "amount",
+ "name": "_amount",
"type": "uint256"
}
],
- "name": "Withdrawn",
- "type": "event"
- },
- {
- "constant": false,
- "inputs": [
- {
- "name": "_unremovableStakingAddress",
- "type": "address"
- }
- ],
- "name": "clearUnremovableValidator",
+ "name": "moveStake",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
@@ -197,8 +79,13 @@
},
{
"constant": false,
- "inputs": [],
- "name": "incrementStakingEpoch",
+ "inputs": [
+ {
+ "name": "_minStake",
+ "type": "uint256"
+ }
+ ],
+ "name": "setDelegatorMinStake",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
@@ -231,132 +118,75 @@
"constant": false,
"inputs": [
{
- "name": "_fromPoolStakingAddress",
- "type": "address"
- },
- {
- "name": "_toPoolStakingAddress",
- "type": "address"
- },
- {
- "name": "_amount",
+ "name": "_minStake",
"type": "uint256"
}
],
- "name": "moveStake",
+ "name": "setCandidateMinStake",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
- "constant": false,
+ "constant": true,
"inputs": [
{
- "name": "_toPoolStakingAddress",
+ "name": "_poolStakingAddress",
"type": "address"
- },
- {
- "name": "_amount",
- "type": "uint256"
}
],
- "name": "stake",
- "outputs": [],
- "payable": false,
- "stateMutability": "nonpayable",
- "type": "function"
- },
- {
- "constant": false,
- "inputs": [
- {
- "name": "_fromPoolStakingAddress",
- "type": "address"
- },
+ "name": "stakeAmountTotal",
+ "outputs": [
{
- "name": "_amount",
+ "name": "",
"type": "uint256"
}
],
- "name": "withdraw",
- "outputs": [],
"payable": false,
- "stateMutability": "nonpayable",
+ "stateMutability": "view",
"type": "function"
},
{
- "constant": false,
+ "constant": true,
"inputs": [
{
"name": "_poolStakingAddress",
"type": "address"
},
{
- "name": "_amount",
- "type": "int256"
- }
- ],
- "name": "orderWithdraw",
- "outputs": [],
- "payable": false,
- "stateMutability": "nonpayable",
- "type": "function"
- },
- {
- "constant": false,
- "inputs": [
- {
- "name": "_poolStakingAddress",
+ "name": "_staker",
"type": "address"
}
],
- "name": "claimOrderedWithdraw",
- "outputs": [],
- "payable": false,
- "stateMutability": "nonpayable",
- "type": "function"
- },
- {
- "constant": false,
- "inputs": [
+ "name": "stakeAmountMinusOrderedWithdraw",
+ "outputs": [
{
- "name": "_erc20TokenContract",
- "type": "address"
+ "name": "",
+ "type": "uint256"
}
],
- "name": "setErc20TokenContract",
- "outputs": [],
"payable": false,
- "stateMutability": "nonpayable",
+ "stateMutability": "view",
"type": "function"
},
{
- "constant": false,
+ "constant": true,
"inputs": [
{
- "name": "_minStake",
- "type": "uint256"
+ "name": "_stakingAddress",
+ "type": "address"
}
],
- "name": "setCandidateMinStake",
- "outputs": [],
- "payable": false,
- "stateMutability": "nonpayable",
- "type": "function"
- },
- {
- "constant": false,
- "inputs": [
+ "name": "poolInactiveIndex",
+ "outputs": [
{
- "name": "_minStake",
+ "name": "",
"type": "uint256"
}
],
- "name": "setDelegatorMinStake",
- "outputs": [],
"payable": false,
- "stateMutability": "nonpayable",
+ "stateMutability": "view",
"type": "function"
},
{
@@ -375,12 +205,21 @@
},
{
"constant": true,
- "inputs": [],
- "name": "getPoolsInactive",
+ "inputs": [
+ {
+ "name": "_poolStakingAddress",
+ "type": "address"
+ },
+ {
+ "name": "_staker",
+ "type": "address"
+ }
+ ],
+ "name": "maxWithdrawAllowed",
"outputs": [
{
"name": "",
- "type": "address[]"
+ "type": "uint256"
}
],
"payable": false,
@@ -389,30 +228,21 @@
},
{
"constant": true,
- "inputs": [],
- "name": "getPoolsLikelihood",
- "outputs": [
+ "inputs": [
{
- "name": "likelihoods",
- "type": "int256[]"
+ "name": "_poolStakingAddress",
+ "type": "address"
},
{
- "name": "sum",
- "type": "int256"
+ "name": "_staker",
+ "type": "address"
}
],
- "payable": false,
- "stateMutability": "view",
- "type": "function"
- },
- {
- "constant": true,
- "inputs": [],
- "name": "getPoolsToBeElected",
+ "name": "stakeAmountByCurrentEpoch",
"outputs": [
{
"name": "",
- "type": "address[]"
+ "type": "uint256"
}
],
"payable": false,
@@ -422,11 +252,11 @@
{
"constant": true,
"inputs": [],
- "name": "getPoolsToBeRemoved",
+ "name": "stakingEpoch",
"outputs": [
{
"name": "",
- "type": "address[]"
+ "type": "uint256"
}
],
"payable": false,
@@ -436,11 +266,11 @@
{
"constant": true,
"inputs": [],
- "name": "areStakeAndWithdrawAllowed",
+ "name": "getDelegatorMinStake",
"outputs": [
{
"name": "",
- "type": "bool"
+ "type": "uint256"
}
],
"payable": false,
@@ -477,8 +307,17 @@
},
{
"constant": true,
- "inputs": [],
- "name": "getDelegatorMinStake",
+ "inputs": [
+ {
+ "name": "_poolStakingAddress",
+ "type": "address"
+ },
+ {
+ "name": "_staker",
+ "type": "address"
+ }
+ ],
+ "name": "maxWithdrawOrderAllowed",
"outputs": [
{
"name": "",
@@ -493,15 +332,19 @@
"constant": true,
"inputs": [
{
- "name": "_stakingAddress",
+ "name": "_poolStakingAddress",
+ "type": "address"
+ },
+ {
+ "name": "_delegator",
"type": "address"
}
],
- "name": "isPoolActive",
+ "name": "poolDelegatorInactiveIndex",
"outputs": [
{
"name": "",
- "type": "bool"
+ "type": "uint256"
}
],
"payable": false,
@@ -510,21 +353,49 @@
},
{
"constant": true,
+ "inputs": [],
+ "name": "getPoolsLikelihood",
+ "outputs": [
+ {
+ "name": "likelihoods",
+ "type": "int256[]"
+ },
+ {
+ "name": "sum",
+ "type": "int256"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": false,
"inputs": [
{
- "name": "_poolStakingAddress",
+ "name": "_unremovableStakingAddress",
"type": "address"
- },
+ }
+ ],
+ "name": "clearUnremovableValidator",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
{
- "name": "_staker",
+ "name": "_poolStakingAddress",
"type": "address"
}
],
- "name": "maxWithdrawAllowed",
+ "name": "poolDelegators",
"outputs": [
{
"name": "",
- "type": "uint256"
+ "type": "address[]"
}
],
"payable": false,
@@ -543,7 +414,7 @@
"type": "address"
}
],
- "name": "maxWithdrawOrderAllowed",
+ "name": "orderWithdrawEpoch",
"outputs": [
{
"name": "",
@@ -581,6 +452,20 @@
"stateMutability": "pure",
"type": "function"
},
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "getPoolsToBeElected",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address[]"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
{
"constant": true,
"inputs": [
@@ -593,7 +478,7 @@
"type": "address"
}
],
- "name": "orderedWithdrawAmount",
+ "name": "stakeAmount",
"outputs": [
{
"name": "",
@@ -608,34 +493,80 @@
"constant": true,
"inputs": [
{
- "name": "_poolStakingAddress",
+ "name": "_stakingAddress",
"type": "address"
}
],
- "name": "orderedWithdrawAmountTotal",
+ "name": "isPoolActive",
"outputs": [
{
"name": "",
- "type": "uint256"
+ "type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_toPoolStakingAddress",
+ "type": "address"
+ },
+ {
+ "name": "_amount",
+ "type": "uint256"
+ }
+ ],
+ "name": "stake",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
{
"constant": true,
+ "inputs": [
+ {
+ "name": "_stakingAddress",
+ "type": "address"
+ }
+ ],
+ "name": "poolIndex",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": false,
"inputs": [
{
"name": "_poolStakingAddress",
"type": "address"
},
{
- "name": "_staker",
- "type": "address"
+ "name": "_amount",
+ "type": "int256"
}
],
- "name": "orderWithdrawEpoch",
+ "name": "orderWithdraw",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "STAKE_UNIT",
"outputs": [
{
"name": "",
@@ -646,6 +577,34 @@
"stateMutability": "view",
"type": "function"
},
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_poolStakingAddress",
+ "type": "address"
+ }
+ ],
+ "name": "claimOrderedWithdraw",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "getPoolsToBeRemoved",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address[]"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
{
"constant": true,
"inputs": [
@@ -654,7 +613,7 @@
"type": "address"
}
],
- "name": "stakeAmountTotal",
+ "name": "orderedWithdrawAmountTotal",
"outputs": [
{
"name": "",
@@ -665,19 +624,51 @@
"stateMutability": "view",
"type": "function"
},
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "getPoolsInactive",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address[]"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "validatorSetContract",
+ "outputs": [
+ {
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
{
"constant": true,
"inputs": [
{
"name": "_poolStakingAddress",
"type": "address"
+ },
+ {
+ "name": "_staker",
+ "type": "address"
}
],
- "name": "poolDelegators",
+ "name": "orderedWithdrawAmount",
"outputs": [
{
"name": "",
- "type": "address[]"
+ "type": "uint256"
}
],
"payable": false,
@@ -686,17 +677,8 @@
},
{
"constant": true,
- "inputs": [
- {
- "name": "_poolStakingAddress",
- "type": "address"
- },
- {
- "name": "_delegator",
- "type": "address"
- }
- ],
- "name": "poolDelegatorIndex",
+ "inputs": [],
+ "name": "MAX_DELEGATORS_PER_POOL",
"outputs": [
{
"name": "",
@@ -711,15 +693,11 @@
"constant": true,
"inputs": [
{
- "name": "_poolStakingAddress",
- "type": "address"
- },
- {
- "name": "_delegator",
+ "name": "_stakingAddress",
"type": "address"
}
],
- "name": "poolDelegatorInactiveIndex",
+ "name": "poolToBeRemovedIndex",
"outputs": [
{
"name": "",
@@ -730,15 +708,19 @@
"stateMutability": "view",
"type": "function"
},
+ {
+ "constant": false,
+ "inputs": [],
+ "name": "incrementStakingEpoch",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
{
"constant": true,
- "inputs": [
- {
- "name": "_stakingAddress",
- "type": "address"
- }
- ],
- "name": "poolIndex",
+ "inputs": [],
+ "name": "MAX_CANDIDATES",
"outputs": [
{
"name": "",
@@ -750,22 +732,21 @@
"type": "function"
},
{
- "constant": true,
+ "constant": false,
"inputs": [
{
- "name": "_stakingAddress",
+ "name": "_fromPoolStakingAddress",
"type": "address"
- }
- ],
- "name": "poolInactiveIndex",
- "outputs": [
+ },
{
- "name": "",
+ "name": "_amount",
"type": "uint256"
}
],
+ "name": "withdraw",
+ "outputs": [],
"payable": false,
- "stateMutability": "view",
+ "stateMutability": "nonpayable",
"type": "function"
},
{
@@ -788,87 +769,223 @@
"type": "function"
},
{
- "constant": true,
+ "anonymous": false,
"inputs": [
{
- "name": "_stakingAddress",
+ "indexed": true,
+ "name": "fromPoolStakingAddress",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "staker",
"type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "stakingEpoch",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "amount",
+ "type": "uint256"
}
],
- "name": "poolToBeRemovedIndex",
- "outputs": [
+ "name": "Claimed",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
{
- "name": "",
+ "indexed": true,
+ "name": "toPoolStakingAddress",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "staker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "stakingEpoch",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "amount",
"type": "uint256"
}
],
- "payable": false,
- "stateMutability": "view",
- "type": "function"
+ "name": "Staked",
+ "type": "event"
},
{
- "constant": true,
+ "anonymous": false,
"inputs": [
{
- "name": "_poolStakingAddress",
+ "indexed": false,
+ "name": "fromPoolStakingAddress",
"type": "address"
},
{
- "name": "_staker",
+ "indexed": true,
+ "name": "toPoolStakingAddress",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "staker",
"type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "stakingEpoch",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "amount",
+ "type": "uint256"
}
],
- "name": "stakeAmount",
- "outputs": [
+ "name": "StakeMoved",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
{
- "name": "",
+ "indexed": true,
+ "name": "fromPoolStakingAddress",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "staker",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "stakingEpoch",
"type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "amount",
+ "type": "int256"
}
],
- "payable": false,
- "stateMutability": "view",
- "type": "function"
+ "name": "WithdrawalOrdered",
+ "type": "event"
},
{
- "constant": true,
+ "anonymous": false,
"inputs": [
{
- "name": "_poolStakingAddress",
+ "indexed": true,
+ "name": "fromPoolStakingAddress",
"type": "address"
},
{
- "name": "_staker",
+ "indexed": true,
+ "name": "staker",
"type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "stakingEpoch",
+ "type": "uint256"
+ },
+ {
+ "indexed": false,
+ "name": "amount",
+ "type": "uint256"
}
],
- "name": "stakeAmountByCurrentEpoch",
- "outputs": [
+ "name": "Withdrawn",
+ "type": "event"
+ },
+ {
+ "constant": false,
+ "inputs": [
{
- "name": "",
+ "name": "_amount",
"type": "uint256"
+ },
+ {
+ "name": "_miningAddress",
+ "type": "address"
}
],
+ "name": "addPool",
+ "outputs": [],
"payable": false,
- "stateMutability": "view",
+ "stateMutability": "nonpayable",
"type": "function"
},
{
- "constant": true,
+ "constant": false,
"inputs": [
{
- "name": "_poolStakingAddress",
+ "name": "_validatorSetContract",
"type": "address"
},
{
- "name": "_staker",
+ "name": "_erc20TokenContract",
"type": "address"
+ },
+ {
+ "name": "_initialStakingAddresses",
+ "type": "address[]"
+ },
+ {
+ "name": "_delegatorMinStake",
+ "type": "uint256"
+ },
+ {
+ "name": "_candidateMinStake",
+ "type": "uint256"
+ },
+ {
+ "name": "_stakingEpochDuration",
+ "type": "uint256"
+ },
+ {
+ "name": "_stakeWithdrawDisallowPeriod",
+ "type": "uint256"
}
],
- "name": "stakeAmountMinusOrderedWithdraw",
+ "name": "initialize",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_blockNumber",
+ "type": "uint256"
+ }
+ ],
+ "name": "setStakingEpochStartBlock",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "areStakeAndWithdrawAllowed",
"outputs": [
{
"name": "",
- "type": "uint256"
+ "type": "bool"
}
],
"payable": false,
@@ -877,13 +994,22 @@
},
{
"constant": true,
- "inputs": [
+ "inputs": [],
+ "name": "stakeWithdrawDisallowPeriod",
+ "outputs": [
{
- "name": "_poolStakingAddress",
- "type": "address"
+ "name": "",
+ "type": "uint256"
}
],
- "name": "stakeAmountTotalMinusOrderedWithdraw",
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "stakingEpochDuration",
"outputs": [
{
"name": "",
@@ -897,7 +1023,7 @@
{
"constant": true,
"inputs": [],
- "name": "stakingEpoch",
+ "name": "stakingEpochStartBlock",
"outputs": [
{
"name": "",
@@ -911,11 +1037,11 @@
{
"constant": true,
"inputs": [],
- "name": "validatorSetContract",
+ "name": "stakingEpochEndBlock",
"outputs": [
{
"name": "",
- "type": "address"
+ "type": "uint256"
}
],
"payable": false,
diff --git a/apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs b/apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs
new file mode 100644
index 0000000000..daf04c22f3
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs
@@ -0,0 +1,12 @@
+defmodule Explorer.Repo.Migrations.AddOldBlockHashForTransactions do
+ use Ecto.Migration
+
+ def change do
+ alter table(:transactions) do
+ # A transient field for deriving old block hash during transaction upserts.
+ # Used to force refetch of a block in case a transaction is re-collated
+ # in a different block. See: https://github.com/poanetwork/blockscout/issues/1911
+ add(:old_block_hash, :bytea, null: true)
+ end
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs b/apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs
new file mode 100644
index 0000000000..70ddac3e03
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs
@@ -0,0 +1,11 @@
+defmodule Explorer.Repo.Migrations.AddRefetchNeededToBlock do
+ use Ecto.Migration
+
+ def change do
+ alter table(:blocks) do
+ add(:refetch_needed, :boolean, default: false)
+ end
+
+ execute("UPDATE blocks SET refetch_needed = TRUE;", "")
+ end
+end
diff --git a/apps/explorer/test/explorer/chain/block_number_cache_test.exs b/apps/explorer/test/explorer/chain/block_number_cache_test.exs
index a33263293c..7b501a718b 100644
--- a/apps/explorer/test/explorer/chain/block_number_cache_test.exs
+++ b/apps/explorer/test/explorer/chain/block_number_cache_test.exs
@@ -3,6 +3,14 @@ defmodule Explorer.Chain.BlockNumberCacheTest do
alias Explorer.Chain.BlockNumberCache
+ setup do
+ Application.put_env(:explorer, Explorer.Chain.BlockNumberCache, enabled: true)
+
+ on_exit(fn ->
+ Application.put_env(:explorer, Explorer.Chain.BlockNumberCache, enabled: false)
+ end)
+ end
+
describe "max_number/1" do
test "returns max number" do
insert(:block, number: 5)
@@ -11,33 +19,6 @@ defmodule Explorer.Chain.BlockNumberCacheTest do
assert BlockNumberCache.max_number() == 5
end
-
- test "invalidates cache if period did pass" do
- insert(:block, number: 5)
-
- BlockNumberCache.setup(cache_period: 2_000)
-
- assert BlockNumberCache.max_number() == 5
-
- insert(:block, number: 10)
-
- Process.sleep(2_000)
-
- assert BlockNumberCache.max_number() == 10
- assert BlockNumberCache.min_number() == 5
- end
-
- test "does not invalidate cache if period time did not pass" do
- insert(:block, number: 5)
-
- BlockNumberCache.setup(cache_period: 10_000)
-
- assert BlockNumberCache.max_number() == 5
-
- insert(:block, number: 10)
-
- assert BlockNumberCache.max_number() == 5
- end
end
describe "min_number/1" do
@@ -48,32 +29,31 @@ defmodule Explorer.Chain.BlockNumberCacheTest do
assert BlockNumberCache.max_number() == 2
end
+ end
- test "invalidates cache" do
- insert(:block, number: 5)
-
- BlockNumberCache.setup(cache_period: 2_000)
+ describe "update/1" do
+ test "updates max number" do
+ insert(:block, number: 2)
- assert BlockNumberCache.min_number() == 5
+ BlockNumberCache.setup()
- insert(:block, number: 2)
+ assert BlockNumberCache.max_number() == 2
- Process.sleep(2_000)
+ assert BlockNumberCache.update(3)
- assert BlockNumberCache.min_number() == 2
- assert BlockNumberCache.max_number() == 5
+ assert BlockNumberCache.max_number() == 3
end
- test "does not invalidate cache if period time did not pass" do
- insert(:block, number: 5)
+ test "updates min number" do
+ insert(:block, number: 2)
- BlockNumberCache.setup(cache_period: 10_000)
+ BlockNumberCache.setup()
- assert BlockNumberCache.max_number() == 5
+ assert BlockNumberCache.min_number() == 2
- insert(:block, number: 2)
+ assert BlockNumberCache.update(1)
- assert BlockNumberCache.max_number() == 5
+ assert BlockNumberCache.min_number() == 1
end
end
end
diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
index ecf1d71dfb..63110cba40 100644
--- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
+++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
@@ -6,7 +6,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
import Explorer.Chain.Import.RunnerCase, only: [insert_address_with_token_balances: 1, update_holder_count!: 2]
alias Ecto.Multi
- alias Explorer.Chain.Import.Runner.{Blocks, Transaction}
+ alias Explorer.Chain.Import.Runner.{Blocks, Transactions}
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.Chain
alias Explorer.Repo
@@ -283,6 +283,29 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
insert_block(new_block1, options)
assert Chain.missing_block_number_ranges(range) == []
end
+
+ # Regression test for https://github.com/poanetwork/blockscout/issues/1911
+ test "forces block refetch if transaction is re-collated in a different block",
+ %{consensus_block: %Block{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do
+ new_block1 = params_for(:block, miner_hash: miner_hash, parent_hash: block_hash, number: block_number + 1)
+ new_block2 = params_for(:block, miner_hash: miner_hash, parent_hash: new_block1.hash, number: block_number + 2)
+
+ range = block_number..(block_number + 2)
+
+ insert_block(new_block1, options)
+ insert_block(new_block2, options)
+ assert Chain.missing_block_number_ranges(range) == []
+
+ trans_hash = transaction_hash()
+
+ transaction1 = transaction_params_with_block([hash: trans_hash], new_block1)
+ insert_transaction(transaction1, options)
+ assert Chain.missing_block_number_ranges(range) == []
+
+ transaction2 = transaction_params_with_block([hash: trans_hash], new_block2)
+ insert_transaction(transaction2, options)
+ assert Chain.missing_block_number_ranges(range) == [(block_number + 1)..(block_number + 1)]
+ end
end
defp insert_block(block_params, options) do
@@ -293,6 +316,28 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
|> Repo.transaction()
end
+ defp transaction_params_with_block(transaction_params, block_params) do
+ params_for(:transaction, transaction_params)
+ |> Map.merge(%{
+ block_hash: block_params.hash,
+ block_number: block_params.number,
+ cumulative_gas_used: 50_000,
+ error: nil,
+ gas_used: 50_000,
+ index: 0,
+ from_address_hash: insert(:address).hash
+ })
+ end
+
+ defp insert_transaction(transaction_params, options) do
+ %Ecto.Changeset{valid?: true, changes: transaction_changes} =
+ Transaction.changeset(%Transaction{}, transaction_params)
+
+ Multi.new()
+ |> Transactions.run([transaction_changes], options)
+ |> Repo.transaction()
+ end
+
defp count(schema) do
Repo.one!(select(schema, fragment("COUNT(*)")))
end
diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
index d5bc6ecfca..af25368679 100644
--- a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
+++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
@@ -1,85 +1,29 @@
defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do
use Explorer.DataCase
+ import Explorer.Factory
+
alias Ecto.Multi
alias Explorer.Chain.Import.Runner.StakingPools
describe "run/1" do
test "insert new pools list" do
- pools = [
- %{
- address_hash: %Explorer.Chain.Hash{
- byte_count: 20,
- bytes: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>
- },
- metadata: %{
- banned_unitil: 0,
- delegators_count: 0,
- is_active: true,
- is_banned: false,
- is_validator: true,
- mining_address: %Explorer.Chain.Hash{
- byte_count: 20,
- bytes: <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>
- },
- retries_count: 1,
- staked_amount: 0,
- was_banned_count: 0,
- was_validator_count: 1
- },
- name: "anonymous",
- primary: true
- },
- %{
- address_hash: %Explorer.Chain.Hash{
- byte_count: 20,
- bytes: <<170, 148, 182, 135, 211, 249, 85, 42, 69, 59, 129, 178, 131, 76, 165, 55, 120, 152, 13, 192>>
- },
- metadata: %{
- banned_unitil: 0,
- delegators_count: 0,
- is_active: true,
- is_banned: false,
- is_validator: true,
- mining_address: %Explorer.Chain.Hash{
- byte_count: 20,
- bytes: <<117, 223, 66, 56, 58, 254, 107, 245, 25, 74, 168, 250, 14, 155, 61, 95, 158, 134, 148, 65>>
- },
- retries_count: 1,
- staked_amount: 0,
- was_banned_count: 0,
- was_validator_count: 1
- },
- name: "anonymous",
- primary: true
- },
- %{
- address_hash: %Explorer.Chain.Hash{
- byte_count: 20,
- bytes: <<49, 44, 35, 14, 125, 109, 176, 82, 36, 246, 2, 8, 166, 86, 227, 84, 28, 92, 66, 186>>
- },
- metadata: %{
- banned_unitil: 0,
- delegators_count: 0,
- is_active: true,
- is_banned: false,
- is_validator: true,
- mining_address: %Explorer.Chain.Hash{
- byte_count: 20,
- bytes: <<82, 45, 243, 150, 174, 112, 160, 88, 189, 105, 119, 132, 8, 99, 15, 219, 2, 51, 137, 178>>
- },
- retries_count: 1,
- staked_amount: 0,
- was_banned_count: 0,
- was_validator_count: 1
- },
- name: "anonymous",
- primary: true
- }
- ]
+ pools = [pool1, pool2, pool3, pool4] = build_list(4, :staking_pool)
assert {:ok, %{insert_staking_pools: list}} = run_changes(pools)
assert Enum.count(list) == Enum.count(pools)
+
+ saved_list =
+ Explorer.Chain.Address.Name
+ |> Repo.all()
+ |> Enum.reduce(%{}, fn pool, acc ->
+ Map.put(acc, pool.address_hash, pool)
+ end)
+
+ assert saved_list[pool1.address_hash].metadata["staked_ratio"] == 0.25
+ assert saved_list[pool2.address_hash].metadata["staked_ratio"] == 0.25
+ assert saved_list[pool3.address_hash].metadata["staked_ratio"] == 0.25
+ assert saved_list[pool4.address_hash].metadata["staked_ratio"] == 0.25
end
end
diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs
index 54ace519bd..fc3977a998 100644
--- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs
+++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs
@@ -1,7 +1,7 @@
defmodule Explorer.Chain.InternalTransactionTest do
use Explorer.DataCase
- alias Explorer.Chain.{InternalTransaction, Wei}
+ alias Explorer.Chain.{Data, InternalTransaction, Wei}
alias Explorer.Factory
import EthereumJSONRPC, only: [integer_to_quantity: 1]
@@ -173,7 +173,7 @@ defmodule Explorer.Chain.InternalTransactionTest do
end
test "it correctly formats a create" do
- contract_code = Factory.contract_code_info().bytecode
+ {:ok, contract_code} = Data.cast(Factory.contract_code_info().bytecode)
contract_address = Factory.address_hash()
from = Factory.address_hash()
gas = 50_000
diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs
index a9b479c3bd..cafd742e2a 100644
--- a/apps/explorer/test/explorer/chain_test.exs
+++ b/apps/explorer/test/explorer/chain_test.exs
@@ -50,6 +50,51 @@ defmodule Explorer.ChainTest do
end
end
+ describe "address_to_logs/2" do
+ test "fetches logs" do
+ address = insert(:address)
+
+ transaction1 =
+ :transaction
+ |> insert(to_address: address)
+ |> with_block()
+
+ insert(:log, transaction: transaction1, index: 1, address: address)
+
+ transaction2 =
+ :transaction
+ |> insert(from_address: address)
+ |> with_block()
+
+ insert(:log, transaction: transaction2, index: 2, address: address)
+
+ assert Enum.count(Chain.address_to_logs(address)) == 2
+ end
+
+ test "paginates logs" do
+ address = insert(:address)
+
+ transaction =
+ :transaction
+ |> insert(to_address: address)
+ |> with_block()
+
+ log1 = insert(:log, transaction: transaction, index: 1, address: address)
+
+ 2..51
+ |> Enum.map(fn index -> insert(:log, transaction: transaction, index: index, address: address) end)
+ |> Enum.map(& &1.index)
+
+ paging_options1 = %PagingOptions{page_size: 1}
+
+ [_log] = Chain.address_to_logs(address, paging_options: paging_options1)
+
+ paging_options2 = %PagingOptions{page_size: 60, key: {transaction.block_number, transaction.index, log1.index}}
+
+ assert Enum.count(Chain.address_to_logs(address, paging_options: paging_options2)) == 50
+ end
+ end
+
describe "address_to_transactions_with_rewards/2" do
test "without transactions" do
address = insert(:address)
@@ -3903,4 +3948,59 @@ defmodule Explorer.ChainTest do
refute Chain.contract_address?(to_string(hash), 1, json_rpc_named_arguments)
end
end
+
+ describe "staking_pools/3" do
+ test "validators staking pools" do
+ inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: true})
+ insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: false})
+
+ options = %PagingOptions{page_size: 20, page_number: 1}
+
+ assert [gotten_validator] = Chain.staking_pools(:validator, options)
+ assert inserted_validator.address_hash == gotten_validator.address_hash
+ end
+
+ test "active staking pools" do
+ inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: true})
+ insert(:address_name, primary: true, metadata: %{is_active: false})
+
+ options = %PagingOptions{page_size: 20, page_number: 1}
+
+ assert [gotten_validator] = Chain.staking_pools(:active, options)
+ assert inserted_validator.address_hash == gotten_validator.address_hash
+ end
+
+ test "inactive staking pools" do
+ insert(:address_name, primary: true, metadata: %{is_active: true})
+ inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: false})
+
+ options = %PagingOptions{page_size: 20, page_number: 1}
+
+ assert [gotten_validator] = Chain.staking_pools(:inactive, options)
+ assert inserted_validator.address_hash == gotten_validator.address_hash
+ end
+ end
+
+ describe "staking_pools_count/1" do
+ test "validators staking pools" do
+ insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: true})
+ insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: false})
+
+ assert Chain.staking_pools_count(:validator) == 1
+ end
+
+ test "active staking pools" do
+ insert(:address_name, primary: true, metadata: %{is_active: true})
+ insert(:address_name, primary: true, metadata: %{is_active: false})
+
+ assert Chain.staking_pools_count(:active) == 1
+ end
+
+ test "inactive staking pools" do
+ insert(:address_name, primary: true, metadata: %{is_active: true})
+ insert(:address_name, primary: true, metadata: %{is_active: false})
+
+ assert Chain.staking_pools_count(:inactive) == 1
+ end
+ end
end
diff --git a/apps/explorer/test/explorer/staking/epoch_counter_test.exs b/apps/explorer/test/explorer/staking/epoch_counter_test.exs
new file mode 100644
index 0000000000..278377ce05
--- /dev/null
+++ b/apps/explorer/test/explorer/staking/epoch_counter_test.exs
@@ -0,0 +1,97 @@
+defmodule Explorer.Staking.EpochCounterTest do
+ use ExUnit.Case, async: false
+
+ import Mox
+
+ alias Explorer.Staking.EpochCounter
+ alias Explorer.Chain.Events.Publisher
+
+ setup :verify_on_exit!
+ setup :set_mox_global
+
+ test "when disabled, it returns nil" do
+ assert EpochCounter.epoch_number() == nil
+ assert EpochCounter.epoch_end_block() == nil
+ end
+
+ test "fetch epoch data" do
+ set_mox(10, 880)
+ Application.put_env(:explorer, EpochCounter, enabled: true)
+ start_supervised!(EpochCounter)
+
+ Process.sleep(1_000)
+
+ assert EpochCounter.epoch_number() == 10
+ assert EpochCounter.epoch_end_block() == 880
+ end
+
+ test "fetch new epoch data" do
+ set_mox(10, 880)
+ Application.put_env(:explorer, EpochCounter, enabled: true)
+ start_supervised!(EpochCounter)
+
+ Process.sleep(1_000)
+
+ assert EpochCounter.epoch_number() == 10
+ assert EpochCounter.epoch_end_block() == 880
+
+ event_type = :blocks
+ broadcast_type = :realtime
+ event_data = [%Explorer.Chain.Block{number: 881}]
+
+ set_mox(11, 960)
+ Publisher.broadcast([{event_type, event_data}], broadcast_type)
+
+ Process.sleep(1_000)
+
+ assert EpochCounter.epoch_number() == 11
+ assert EpochCounter.epoch_end_block() == 960
+ end
+
+ defp set_mox(epoch_num, end_block_num) do
+ expect(
+ EthereumJSONRPC.Mox,
+ :json_rpc,
+ fn [
+ %{
+ id: 0,
+ jsonrpc: "2.0",
+ method: "eth_call",
+ params: _
+ },
+ %{
+ id: 1,
+ jsonrpc: "2.0",
+ method: "eth_call",
+ params: _
+ }
+ ],
+ _options ->
+ {:ok,
+ [
+ %{
+ id: 0,
+ jsonrpc: "2.0",
+ result: encode_num(epoch_num)
+ },
+ %{
+ id: 1,
+ jsonrpc: "2.0",
+ result: encode_num(end_block_num)
+ }
+ ]}
+ end
+ )
+ end
+
+ defp encode_num(num) do
+ selector = %ABI.FunctionSelector{function: nil, types: [uint: 32]}
+
+ encoded_num =
+ [num]
+ |> ABI.TypeEncoder.encode(selector)
+ |> Base.encode16(case: :lower)
+
+ "0x" <> encoded_num
+ end
+end
diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs
index ac6a600722..bb3af9fbcc 100644
--- a/apps/explorer/test/explorer/staking/pools_reader_test.exs
+++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs
@@ -1,6 +1,5 @@
defmodule Explorer.Token.PoolsReaderTest do
use EthereumJSONRPC.Case
- use Explorer.DataCase
alias Explorer.Staking.PoolsReader
@@ -44,6 +43,7 @@ defmodule Explorer.Token.PoolsReaderTest do
mining_address:
<<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>,
staked_amount: 0,
+ self_staked_amount: 0,
staking_address: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>,
was_banned_count: 0,
was_validator_count: 2
@@ -162,6 +162,25 @@ defmodule Explorer.Token.PoolsReaderTest do
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
+ # stakeAmountMinusOrderedWithdraw
+ %{
+ id: id,
+ jsonrpc: "2.0",
+ method: "eth_call",
+ params: [
+ %{
+ data:
+ "0x58daab6a0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf60000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6",
+ to: _
+ },
+ "latest"
+ ]
+ } ->
+ %{
+ id: id,
+ result: "0x0000000000000000000000000000000000000000000000000000000000000000"
+ }
+
# isValidator
%{
id: id,
diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex
index 75d3429e70..68bb21434c 100644
--- a/apps/explorer/test/support/data_case.ex
+++ b/apps/explorer/test/support/data_case.ex
@@ -39,7 +39,7 @@ defmodule Explorer.DataCase do
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()})
end
- Explorer.Chain.BlockNumberCache.setup(cache_period: 0)
+ Explorer.Chain.BlockNumberCache.setup()
:ok
end
diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex
index 4c030a4db1..115b07e066 100644
--- a/apps/explorer/test/support/factory.ex
+++ b/apps/explorer/test/support/factory.ex
@@ -609,4 +609,24 @@ defmodule Explorer.Factory do
user: build(:user)
}
end
+
+ def staking_pool_factory do
+ %{
+ address_hash: address_hash(),
+ metadata: %{
+ banned_unitil: 0,
+ delegators_count: 0,
+ is_active: true,
+ is_banned: false,
+ is_validator: true,
+ mining_address: address_hash(),
+ retries_count: 1,
+ staked_amount: 25,
+ was_banned_count: 0,
+ was_validator_count: 1
+ },
+ name: "anonymous",
+ primary: true
+ }
+ end
end
diff --git a/apps/indexer/README.md b/apps/indexer/README.md
index 34fb6e6ef3..173df0c6ab 100644
--- a/apps/indexer/README.md
+++ b/apps/indexer/README.md
@@ -92,6 +92,7 @@ After all deployed instances get all needed data, these fetchers should be depre
- `uncataloged_token_transfers`: extracts token transfers from logs, which previously weren't parsed due to unknown format
- `uncles_without_index`: adds previously unfetched `index` field for unfetched blocks in `block_second_degree_relations`
+- `blocks_transactions_mismatch`: refetches each block once and revokes consensus to those whose transaction number mismatches with the number currently stored. This is meant to force the correction of a race condition that caused successfully fetched transactions to be overwritten by a following non-consensus block: [#1911](https://github.com/poanetwork/blockscout/issues/1911).
## Memory Usage
diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex
index deb0e4554f..06a702aa4b 100644
--- a/apps/indexer/lib/indexer/block/fetcher.ex
+++ b/apps/indexer/lib/indexer/block/fetcher.ex
@@ -11,7 +11,7 @@ defmodule Indexer.Block.Fetcher do
alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries}
alias Explorer.Chain
- alias Explorer.Chain.{Address, Block, Hash, Import, Transaction}
+ alias Explorer.Chain.{Address, Block, BlockNumberCache, Hash, Import, Transaction}
alias Indexer.Block.Fetcher.Receipts
alias Indexer.Fetcher.{
@@ -171,13 +171,23 @@ defmodule Indexer.Block.Fetcher do
transactions: %{params: transactions_with_receipts}
}
) do
- {:ok, %{inserted: inserted, errors: blocks_errors}}
+ result = {:ok, %{inserted: inserted, errors: blocks_errors}}
+ update_block_cache(inserted[:blocks])
+ result
else
{step, {:error, reason}} -> {:error, {step, reason}}
{:import, {:error, step, failed_value, changes_so_far}} -> {:error, {step, failed_value, changes_so_far}}
end
end
+ defp update_block_cache(blocks) do
+ max_block = Enum.max_by(blocks, fn block -> block.number end)
+ min_block = Enum.min_by(blocks, fn block -> block.number end)
+
+ BlockNumberCache.update(max_block.number)
+ BlockNumberCache.update(min_block.number)
+ end
+
def import(
%__MODULE__{broadcast: broadcast, callback_module: callback_module} = state,
options
diff --git a/apps/indexer/lib/indexer/fetcher/staking_pools.ex b/apps/indexer/lib/indexer/fetcher/staking_pools.ex
index 68794d9ee4..fe4ab84c28 100644
--- a/apps/indexer/lib/indexer/fetcher/staking_pools.ex
+++ b/apps/indexer/lib/indexer/fetcher/staking_pools.ex
@@ -125,6 +125,7 @@ defmodule Indexer.Fetcher.StakingPools do
pool
|> Map.delete(:staking_address)
|> Map.put(:mining_address, mining_address)
+ |> Map.put(:is_pool, true)
%{
name: "anonymous",
diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex
index 21dc3637ec..b2c3d19664 100644
--- a/apps/indexer/lib/indexer/supervisor.ex
+++ b/apps/indexer/lib/indexer/supervisor.ex
@@ -24,6 +24,7 @@ defmodule Indexer.Supervisor do
}
alias Indexer.Temporary.{
+ BlocksTransactionsMismatch,
UncatalogedTokenTransfers,
UnclesWithoutIndex
}
@@ -124,6 +125,8 @@ defmodule Indexer.Supervisor do
# Temporary workers
{UncatalogedTokenTransfers.Supervisor, [[]]},
{UnclesWithoutIndex.Supervisor,
+ [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
+ {BlocksTransactionsMismatch.Supervisor,
[[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}
],
strategy: :one_for_one
diff --git a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
new file mode 100644
index 0000000000..76f4322379
--- /dev/null
+++ b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
@@ -0,0 +1,115 @@
+defmodule Indexer.Temporary.BlocksTransactionsMismatch do
+ @moduledoc """
+ Fetches `consensus` `t:Explorer.Chain.Block.t/0` and compares their transaction
+ number against a node, to revoke `consensus` on mismatch.
+
+ This is meant to fix incorrectly strored transactions that happened as a result
+ of a race condition due to the asynchronicity of indexer's components.
+ """
+
+ use Indexer.Fetcher
+
+ require Logger
+
+ import Ecto.Query
+
+ alias Ecto.Multi
+ alias EthereumJSONRPC.Blocks
+ alias Explorer.Chain.Block
+ alias Explorer.Repo
+ alias Indexer.BufferedTask
+
+ @behaviour BufferedTask
+
+ @defaults [
+ flush_interval: :timer.seconds(3),
+ max_batch_size: 10,
+ max_concurrency: 4,
+ task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor,
+ metadata: [fetcher: :blocks_transactions_mismatch]
+ ]
+
+ @doc false
+ def child_spec([init_options, gen_server_options]) when is_list(init_options) do
+ {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments)
+
+ unless state do
+ raise ArgumentError,
+ ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <>
+ "to allow for json_rpc calls when running."
+ end
+
+ merged_init_options =
+ @defaults
+ |> Keyword.merge(mergeable_init_options)
+ |> Keyword.put(:state, state)
+
+ Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__)
+ end
+
+ @impl BufferedTask
+ def init(initial, reducer, _) do
+ query =
+ from(block in Block,
+ join: transactions in assoc(block, :transactions),
+ where: block.consensus and block.refetch_needed,
+ group_by: block.hash,
+ select: {block, count(transactions.hash)}
+ )
+
+ {:ok, final} = Repo.stream_reduce(query, initial, &reducer.(&1, &2))
+
+ final
+ end
+
+ @impl BufferedTask
+ def run(blocks_data, json_rpc_named_arguments) do
+ hashes = Enum.map(blocks_data, fn {block, _trans_num} -> block.hash end)
+
+ Logger.debug("fetching")
+
+ case EthereumJSONRPC.fetch_blocks_by_hash(hashes, json_rpc_named_arguments) do
+ {:ok, blocks} ->
+ run_blocks(blocks, blocks_data)
+
+ {:error, reason} ->
+ Logger.error(fn -> ["failed to fetch: ", inspect(reason)] end)
+ {:retry, blocks_data}
+ end
+ end
+
+ defp run_blocks(%Blocks{blocks_params: []}, blocks_data), do: {:retry, blocks_data}
+
+ defp run_blocks(
+ %Blocks{transactions_params: transactions_params},
+ blocks_data
+ ) do
+ found_blocks_map =
+ transactions_params
+ |> Enum.group_by(&Map.fetch!(&1, :block_hash))
+ |> Map.new(fn {block_hash, trans_lst} -> {block_hash, Enum.count(trans_lst)} end)
+
+ {found_blocks_data, missing_blocks_data} =
+ Enum.split_with(blocks_data, fn {block, _trans_num} ->
+ Map.has_key?(found_blocks_map, to_string(block.hash))
+ end)
+
+ {:ok, _} =
+ found_blocks_data
+ |> Enum.reduce(Multi.new(), fn {block, trans_num}, multi ->
+ changes = %{
+ refetch_needed: false,
+ consensus: found_blocks_map[to_string(block.hash)] == trans_num
+ }
+
+ Multi.update(multi, block.hash, Block.changeset(block, changes))
+ end)
+ |> Repo.transaction()
+
+ if Enum.empty?(missing_blocks_data) do
+ :ok
+ else
+ {:retry, missing_blocks_data}
+ end
+ end
+end
diff --git a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs
index 8f985537bf..13e2c0d7ee 100644
--- a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs
+++ b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs
@@ -129,6 +129,25 @@ defmodule Indexer.Fetcher.StakingPoolsTest do
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
+ # stakeAmountMinusOrderedWithdraw
+ %{
+ id: id,
+ jsonrpc: "2.0",
+ method: "eth_call",
+ params: [
+ %{
+ data:
+ "0x58daab6a0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf60000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6",
+ to: _
+ },
+ "latest"
+ ]
+ } ->
+ %{
+ id: id,
+ result: "0x0000000000000000000000000000000000000000000000000000000000000000"
+ }
+
# isValidator
%{
id: id,