* Use `Explorer.Chain.stream_unfetched_uncle_hashes` to initialize queue. * Have `Indexer.Block.Catchup.Fetcher` and `Indexer.Block.Realtime.Fetcher` queue uncles to fetch by calling `Indexer.Block.Uncle.Fetcher.async_fetch_blocks`.pull/802/head
parent
12f789a96d
commit
8e8b4b5508
@ -0,0 +1,154 @@ |
|||||||
|
defmodule Indexer.Block.Uncle.Fetcher do |
||||||
|
@moduledoc """ |
||||||
|
Fetches `t:Explorer.Chain.Block.t/0` by `hash` and updates `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` |
||||||
|
`uncle_fetched_at` where the `uncle_hash` matches `hash`. |
||||||
|
""" |
||||||
|
|
||||||
|
require Logger |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.Chain.Hash |
||||||
|
alias Indexer.{AddressExtraction, Block, BufferedTask} |
||||||
|
|
||||||
|
@behaviour Block.Fetcher |
||||||
|
@behaviour BufferedTask |
||||||
|
|
||||||
|
@defaults [ |
||||||
|
flush_interval: :timer.seconds(3), |
||||||
|
max_batch_size: 10, |
||||||
|
max_concurrency: 10, |
||||||
|
init_chunk_size: 1000, |
||||||
|
task_supervisor: Indexer.Block.Uncle.TaskSupervisor |
||||||
|
] |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Asynchronously fetches `t:Explorer.Chain.Block.t/0` for the given `hashes` and updates |
||||||
|
`t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `block_fetched_at`. |
||||||
|
""" |
||||||
|
@spec async_fetch_blocks([Hash.Full.t()]) :: :ok |
||||||
|
def async_fetch_blocks(block_hashes) when is_list(block_hashes) do |
||||||
|
BufferedTask.buffer( |
||||||
|
__MODULE__, |
||||||
|
block_hashes |
||||||
|
|> Enum.map(&to_string/1) |
||||||
|
|> Enum.uniq() |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
@doc false |
||||||
|
def child_spec([init_options, gen_server_options]) when is_list(init_options) do |
||||||
|
{state, mergeable_init_options} = Keyword.pop(init_options, :block_fetcher) |
||||||
|
|
||||||
|
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, %Block.Fetcher{state | broadcast: false, callback_module: __MODULE__}) |
||||||
|
|
||||||
|
Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__) |
||||||
|
end |
||||||
|
|
||||||
|
@impl BufferedTask |
||||||
|
def init(initial, reducer, _) do |
||||||
|
{:ok, final} = |
||||||
|
Chain.stream_unfetched_uncle_hashes(initial, fn uncle_hash, acc -> |
||||||
|
uncle_hash |
||||||
|
|> to_string() |
||||||
|
|> reducer.(acc) |
||||||
|
end) |
||||||
|
|
||||||
|
final |
||||||
|
end |
||||||
|
|
||||||
|
@impl BufferedTask |
||||||
|
def run(hashes, _retries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} = block_fetcher) do |
||||||
|
# the same block could be included as an uncle on multiple blocks, but we only want to fetch it once |
||||||
|
unique_hashes = Enum.uniq(hashes) |
||||||
|
|
||||||
|
Logger.debug(fn -> "fetching #{length(unique_hashes)} uncle blocks" end) |
||||||
|
|
||||||
|
case EthereumJSONRPC.fetch_blocks_by_hash(unique_hashes, json_rpc_named_arguments) do |
||||||
|
{:ok, |
||||||
|
%{ |
||||||
|
blocks: blocks_params, |
||||||
|
transactions: transactions_params, |
||||||
|
block_second_degree_relations: block_second_degree_relations_params |
||||||
|
}} -> |
||||||
|
addresses_params = |
||||||
|
AddressExtraction.extract_addresses(%{blocks: blocks_params, transactions: transactions_params}) |
||||||
|
|
||||||
|
{:ok, _} = |
||||||
|
Block.Fetcher.import(block_fetcher, %{ |
||||||
|
addresses: %{params: addresses_params}, |
||||||
|
blocks: %{params: blocks_params}, |
||||||
|
block_second_degree_relations: %{params: block_second_degree_relations_params}, |
||||||
|
transactions: %{params: transactions_params, on_conflict: :nothing} |
||||||
|
}) |
||||||
|
|
||||||
|
:ok |
||||||
|
|
||||||
|
{:error, reason} -> |
||||||
|
Logger.debug(fn -> "failed to fetch #{length(unique_hashes)} uncle blocks, #{inspect(reason)}" end) |
||||||
|
{:retry, unique_hashes} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@impl Block.Fetcher |
||||||
|
def import(_, options) when is_map(options) do |
||||||
|
with {:ok, %{block_second_degree_relations: block_second_degree_relations}} = ok <- |
||||||
|
options |
||||||
|
|> uncle_blocks() |
||||||
|
|> fork_transactions() |
||||||
|
|> Chain.import() do |
||||||
|
# * CoinBalance.Fetcher.async_fetch_balances is not called because uncles don't affect balances |
||||||
|
# * InternalTransaction.Fetcher.async_fetch is not called because internal transactions are based on transaction |
||||||
|
# hash, which is shared with transaction on consensus blocks. |
||||||
|
# * Token.Fetcher.async_fetch is not called because the tokens only matter on consensus blocks |
||||||
|
# * TokenBalance.Fetcher.async_fetch is not called because it uses block numbers from consensus, not uncles |
||||||
|
|
||||||
|
block_second_degree_relations |
||||||
|
|> Enum.map(& &1.uncle_hash) |
||||||
|
|> Block.Uncle.Fetcher.async_fetch_blocks() |
||||||
|
|
||||||
|
ok |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp uncle_blocks(chain_import_options) do |
||||||
|
put_in(chain_import_options, [:blocks, :params, Access.all(), :consensus], false) |
||||||
|
end |
||||||
|
|
||||||
|
defp fork_transactions(chain_import_options) do |
||||||
|
transactions_params = chain_import_options[:transactions][:params] || [] |
||||||
|
|
||||||
|
chain_import_options |
||||||
|
|> put_in([:transactions, :params], forked_transactions_params(transactions_params)) |
||||||
|
|> put_in([Access.key(:transaction_forks, %{}), :params], transaction_forks_params(transactions_params)) |
||||||
|
end |
||||||
|
|
||||||
|
defp forked_transactions_params(transactions_params) do |
||||||
|
# With no block_hash, there will be a collision for the same hash when a transaction is used in more than 1 uncle, |
||||||
|
# so use MapSet to prevent duplicate row errors. |
||||||
|
MapSet.new(transactions_params, fn transaction_params -> |
||||||
|
Map.merge(transaction_params, %{ |
||||||
|
block_hash: nil, |
||||||
|
block_number: nil, |
||||||
|
index: nil, |
||||||
|
gas_used: nil, |
||||||
|
cumulative_gas_used: nil, |
||||||
|
status: nil |
||||||
|
}) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp transaction_forks_params(transactions_params) do |
||||||
|
Enum.map(transactions_params, fn %{block_hash: uncle_hash, index: index, hash: hash} -> |
||||||
|
%{uncle_hash: uncle_hash, index: index, hash: hash} |
||||||
|
end) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,38 @@ |
|||||||
|
defmodule Indexer.Block.Uncle.Supervisor do |
||||||
|
@moduledoc """ |
||||||
|
Supervises `Indexer.Block.Uncle.Fetcher`. |
||||||
|
""" |
||||||
|
|
||||||
|
use Supervisor |
||||||
|
|
||||||
|
alias Indexer.Block.Uncle.Fetcher |
||||||
|
|
||||||
|
def child_spec([init_arguments]) do |
||||||
|
child_spec([init_arguments, []]) |
||||||
|
end |
||||||
|
|
||||||
|
def child_spec([_init_arguments, _gen_server_options] = start_link_arguments) do |
||||||
|
default = %{ |
||||||
|
id: __MODULE__, |
||||||
|
start: {__MODULE__, :start_link, start_link_arguments}, |
||||||
|
type: :supervisor |
||||||
|
} |
||||||
|
|
||||||
|
Supervisor.child_spec(default, []) |
||||||
|
end |
||||||
|
|
||||||
|
def start_link(arguments, gen_server_options \\ []) do |
||||||
|
Supervisor.start_link(__MODULE__, arguments, gen_server_options) |
||||||
|
end |
||||||
|
|
||||||
|
@impl Supervisor |
||||||
|
def init(fetcher_arguments) do |
||||||
|
Supervisor.init( |
||||||
|
[ |
||||||
|
{Task.Supervisor, name: Indexer.Block.Uncle.TaskSupervisor}, |
||||||
|
{Fetcher, [fetcher_arguments, [name: Fetcher]]} |
||||||
|
], |
||||||
|
strategy: :rest_for_one |
||||||
|
) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,115 @@ |
|||||||
|
defmodule Indexer.Block.Catchup.FetcherTest do |
||||||
|
use EthereumJSONRPC.Case, async: false |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
import Mox |
||||||
|
|
||||||
|
alias Indexer.{Block, CoinBalance, InternalTransaction, Token, TokenBalance} |
||||||
|
alias Indexer.Block.Catchup.Fetcher |
||||||
|
|
||||||
|
@moduletag capture_log: true |
||||||
|
|
||||||
|
# MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to |
||||||
|
# use expectations and stubs from test's pid. |
||||||
|
setup :set_mox_global |
||||||
|
|
||||||
|
setup :verify_on_exit! |
||||||
|
|
||||||
|
setup do |
||||||
|
# Uncle don't occur on POA chains, so there's no way to test this using the public addresses, so mox-only testing |
||||||
|
%{ |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [], |
||||||
|
# Which one does not matter, so pick one |
||||||
|
variant: EthereumJSONRPC.Parity |
||||||
|
] |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
describe "import/1" do |
||||||
|
test "fetches uncles asynchronously", %{json_rpc_named_arguments: json_rpc_named_arguments} do |
||||||
|
CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) |
||||||
|
InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) |
||||||
|
Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) |
||||||
|
TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) |
||||||
|
|
||||||
|
parent = self() |
||||||
|
|
||||||
|
pid = |
||||||
|
spawn_link(fn -> |
||||||
|
receive do |
||||||
|
{:"$gen_call", from, {:buffer, uncles}} -> |
||||||
|
GenServer.reply(from, :ok) |
||||||
|
send(parent, {:uncles, uncles}) |
||||||
|
end |
||||||
|
end) |
||||||
|
|
||||||
|
Process.register(pid, Block.Uncle.Fetcher) |
||||||
|
|
||||||
|
nephew_hash = block_hash() |> to_string() |
||||||
|
uncle_hash = block_hash() |> to_string() |
||||||
|
miner_hash = address_hash() |> to_string() |
||||||
|
block_number = 0 |
||||||
|
|
||||||
|
assert {:ok, _} = |
||||||
|
Fetcher.import(%Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}, %{ |
||||||
|
addresses: %{ |
||||||
|
params: [ |
||||||
|
%{hash: miner_hash} |
||||||
|
] |
||||||
|
}, |
||||||
|
address_hash_to_fetched_balance_block_number: %{miner_hash => block_number}, |
||||||
|
balances: %{ |
||||||
|
params: [ |
||||||
|
%{ |
||||||
|
address_hash: miner_hash, |
||||||
|
block_number: block_number |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
blocks: %{ |
||||||
|
params: [ |
||||||
|
%{ |
||||||
|
difficulty: 0, |
||||||
|
gas_limit: 21000, |
||||||
|
gas_used: 21000, |
||||||
|
miner_hash: miner_hash, |
||||||
|
nonce: 0, |
||||||
|
number: block_number, |
||||||
|
parent_hash: |
||||||
|
block_hash() |
||||||
|
|> to_string(), |
||||||
|
size: 0, |
||||||
|
timestamp: DateTime.utc_now(), |
||||||
|
total_difficulty: 0, |
||||||
|
hash: nephew_hash |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
block_second_degree_relations: %{ |
||||||
|
params: [ |
||||||
|
%{ |
||||||
|
nephew_hash: nephew_hash, |
||||||
|
uncle_hash: uncle_hash |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
tokens: %{ |
||||||
|
params: [], |
||||||
|
on_conflict: :nothing |
||||||
|
}, |
||||||
|
token_balances: %{ |
||||||
|
params: [] |
||||||
|
}, |
||||||
|
transactions: %{ |
||||||
|
params: [], |
||||||
|
on_conflict: :nothing |
||||||
|
}, |
||||||
|
transaction_hash_to_block_number: %{} |
||||||
|
}) |
||||||
|
|
||||||
|
assert_receive {:uncles, [^uncle_hash]} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,126 @@ |
|||||||
|
defmodule Indexer.Block.Uncle.FetcherTest do |
||||||
|
# MUST be `async: false` so that {:shared, pid} is set for connection to allow CoinBalanceFetcher's self-send to have |
||||||
|
# connection allowed immediately. |
||||||
|
use EthereumJSONRPC.Case, async: false |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Chain |
||||||
|
alias Indexer.Block |
||||||
|
|
||||||
|
import Mox |
||||||
|
|
||||||
|
@moduletag :capture_log |
||||||
|
|
||||||
|
# MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to |
||||||
|
# use expectations and stubs from test's pid. |
||||||
|
setup :set_mox_global |
||||||
|
|
||||||
|
setup :verify_on_exit! |
||||||
|
|
||||||
|
setup do |
||||||
|
# Uncle don't occur on POA chains, so there's no way to test this using the public addresses, so mox-only testing |
||||||
|
%{ |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [], |
||||||
|
# Which one does not matter, so pick one |
||||||
|
variant: EthereumJSONRPC.Parity |
||||||
|
] |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
describe "child_spec/1" do |
||||||
|
test "raises ArgumentError is `json_rpc_named_arguments is not provided" do |
||||||
|
assert_raise ArgumentError, |
||||||
|
":json_rpc_named_arguments must be provided to `Elixir.Indexer.Block.Uncle.Fetcher.child_spec " <> |
||||||
|
"to allow for json_rpc calls when running.", |
||||||
|
fn -> |
||||||
|
start_supervised({Block.Uncle.Fetcher, [[], []]}) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "init/1" do |
||||||
|
test "fetched unfetched uncle hashes", %{json_rpc_named_arguments: json_rpc_named_arguments} do |
||||||
|
assert %Chain.Block.SecondDegreeRelation{nephew_hash: nephew_hash, uncle_hash: uncle_hash, uncle: nil} = |
||||||
|
:block_second_degree_relation |
||||||
|
|> insert() |
||||||
|
|> Repo.preload([:nephew, :uncle]) |
||||||
|
|
||||||
|
uncle_hash_data = to_string(uncle_hash) |
||||||
|
uncle_uncle_hash_data = to_string(block_hash()) |
||||||
|
|
||||||
|
EthereumJSONRPC.Mox |
||||||
|
|> expect(:json_rpc, fn [%{method: "eth_getBlockByHash", params: [^uncle_hash_data, true]}], _ -> |
||||||
|
number_quantity = "0x0" |
||||||
|
|
||||||
|
{:ok, |
||||||
|
[ |
||||||
|
%{ |
||||||
|
result: %{ |
||||||
|
"author" => "0xe2ac1c6843a33f81ae4935e5ef1277a392990381", |
||||||
|
"difficulty" => "0xfffffffffffffffffffffffffffffffe", |
||||||
|
"extraData" => "0xd583010a068650617269747986312e32362e32826c69", |
||||||
|
"gasLimit" => "0x7a1200", |
||||||
|
"gasUsed" => "0x0", |
||||||
|
"hash" => uncle_hash_data, |
||||||
|
"logsBloom" => "0x", |
||||||
|
"miner" => "0xe2ac1c6843a33f81ae4935e5ef1277a392990381", |
||||||
|
"number" => number_quantity, |
||||||
|
"parentHash" => "0x006edcaa1e6fde822908783bc4ef1ad3675532d542fce53537557391cfe34c3c", |
||||||
|
"size" => "0x243", |
||||||
|
"timestamp" => "0x5b437f41", |
||||||
|
"totalDifficulty" => "0x342337ffffffffffffffffffffffffed8d29bb", |
||||||
|
"transactions" => [ |
||||||
|
%{ |
||||||
|
"blockHash" => uncle_hash_data, |
||||||
|
"blockNumber" => number_quantity, |
||||||
|
"chainId" => "0x4d", |
||||||
|
"condition" => nil, |
||||||
|
"creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4", |
||||||
|
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", |
||||||
|
"gas" => "0x47b760", |
||||||
|
"gasPrice" => "0x174876e800", |
||||||
|
"hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", |
||||||
|
"input" => "0x", |
||||||
|
"nonce" => "0x0", |
||||||
|
"r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75", |
||||||
|
"s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3", |
||||||
|
"standardV" => "0x0", |
||||||
|
"to" => nil, |
||||||
|
"transactionIndex" => "0x0", |
||||||
|
"v" => "0xbd", |
||||||
|
"value" => "0x0" |
||||||
|
} |
||||||
|
], |
||||||
|
"uncles" => [uncle_uncle_hash_data] |
||||||
|
} |
||||||
|
} |
||||||
|
]} |
||||||
|
end) |
||||||
|
|
||||||
|
Block.Uncle.Supervisor.Case.start_supervised!( |
||||||
|
block_fetcher: %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} |
||||||
|
) |
||||||
|
|
||||||
|
wait(fn -> |
||||||
|
Repo.one!( |
||||||
|
from(bsdr in Chain.Block.SecondDegreeRelation, |
||||||
|
where: bsdr.nephew_hash == ^nephew_hash and not is_nil(bsdr.uncle_fetched_at) |
||||||
|
) |
||||||
|
) |
||||||
|
end) |
||||||
|
|
||||||
|
refute is_nil(Repo.get(Chain.Block, uncle_hash)) |
||||||
|
assert Repo.aggregate(Chain.Transaction.Fork, :count, :hash) == 1 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp wait(producer) do |
||||||
|
producer.() |
||||||
|
rescue |
||||||
|
Ecto.NoResultsError -> |
||||||
|
Process.sleep(100) |
||||||
|
wait(producer) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,18 @@ |
|||||||
|
defmodule Indexer.Block.Uncle.Supervisor.Case do |
||||||
|
alias Indexer.Block |
||||||
|
|
||||||
|
def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do |
||||||
|
merged_fetcher_arguments = |
||||||
|
Keyword.merge( |
||||||
|
fetcher_arguments, |
||||||
|
flush_interval: 50, |
||||||
|
init_chunk_size: 1, |
||||||
|
max_batch_size: 1, |
||||||
|
max_concurrency: 1 |
||||||
|
) |
||||||
|
|
||||||
|
[merged_fetcher_arguments] |
||||||
|
|> Block.Uncle.Supervisor.child_spec() |
||||||
|
|> ExUnit.Callbacks.start_supervised!() |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue