From c8fb42aba0c4f4cc3a9accc1f4db33c0c3d6c11f Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Fri, 11 Jan 2019 13:44:31 -0500 Subject: [PATCH 1/4] fix: poll on import completion and only new blocks --- apps/indexer/lib/indexer/block/realtime/fetcher.ex | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 36c9425ee5..73bc9480ed 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -54,8 +54,12 @@ defmodule Indexer.Block.Realtime.Fetcher do @impl GenServer def handle_continue({:init, subscribe_named_arguments}, %__MODULE__{subscription: nil} = state) when is_list(subscribe_named_arguments) do + case EthereumJSONRPC.subscribe("newHeads", subscribe_named_arguments) do - {:ok, subscription} -> {:noreply, %__MODULE__{state | subscription: subscription}} + {:ok, subscription} -> + schedule_polling() + + {:noreply, %__MODULE__{state | subscription: subscription}} {:error, reason} -> {:stop, reason, state} end end @@ -79,8 +83,6 @@ defmodule Indexer.Block.Realtime.Fetcher do new_max_number = new_max_number(number, max_number_seen) - schedule_polling() - {:noreply, %{ state @@ -100,11 +102,10 @@ defmodule Indexer.Block.Realtime.Fetcher do ) do {number, new_max_number} = case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do - {:ok, number} -> + {:ok, number} when number > max_number_seen -> start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen) - {number, new_max_number(number, max_number_seen)} - + {max_number_seen, number} {:error, _} -> {previous_number, max_number_seen} end From e3c4703a9958c9c0a72ddae9420f589bab58b741 Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Fri, 11 Jan 2019 16:06:47 -0500 Subject: [PATCH 2/4] fixup! fix: poll on import completion and only new blocks --- .../explorer/counters/average_block_time.ex | 16 +++--- .../lib/indexer/block/realtime/fetcher.ex | 53 ++++++++++++------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/apps/explorer/lib/explorer/counters/average_block_time.ex b/apps/explorer/lib/explorer/counters/average_block_time.ex index 5bfce4082f..11e26e266b 100644 --- a/apps/explorer/lib/explorer/counters/average_block_time.ex +++ b/apps/explorer/lib/explorer/counters/average_block_time.ex @@ -64,13 +64,17 @@ defmodule Explorer.Counters.AverageBlockTime do # This is pretty naive, but we'll only ever be sorting 100 dates so I don't think # complex logic is really necessary here. - defp add_block(%{timestamps: timestamps} = state, block) do - timestamps = - [block | timestamps] - |> Enum.sort_by(fn {number, _} -> number end, &Kernel.>/2) - |> Enum.take(100) + defp add_block(%{timestamps: timestamps} = state, {new_number, _} = block) do + if Enum.any?(timestamps, fn {number, _} -> number == new_number end) do + state + else + timestamps = + [block | timestamps] + |> Enum.sort_by(fn {number, _} -> number end, &Kernel.>/2) + |> Enum.take(100) - %{state | timestamps: timestamps, average: average_distance(timestamps)} + %{state | timestamps: timestamps, average: average_distance(timestamps)} + end end defp average_distance([]), do: Duration.from_milliseconds(0) diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 73bc9480ed..16bef60381 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -15,15 +15,15 @@ defmodule Indexer.Block.Realtime.Fetcher do alias Ecto.Changeset alias EthereumJSONRPC.{FetchedBalances, Subscription} alias Explorer.Chain + alias Explorer.Counters.AverageBlockTime alias Indexer.{AddressExtraction, Block, TokenBalances, Tracer} alias Indexer.Block.Realtime.{ConsensusEnsurer, TaskSupervisor} - - @polling_period 2_000 + alias Timex.Duration @behaviour Block.Fetcher @enforce_keys ~w(block_fetcher)a - defstruct ~w(block_fetcher subscription previous_number max_number_seen)a + defstruct ~w(block_fetcher subscription previous_number max_number_seen timer)a @type t :: %__MODULE__{ block_fetcher: %Block.Fetcher{ @@ -54,13 +54,14 @@ defmodule Indexer.Block.Realtime.Fetcher do @impl GenServer def handle_continue({:init, subscribe_named_arguments}, %__MODULE__{subscription: nil} = state) when is_list(subscribe_named_arguments) do - case EthereumJSONRPC.subscribe("newHeads", subscribe_named_arguments) do {:ok, subscription} -> - schedule_polling() + timer = schedule_polling() - {:noreply, %__MODULE__{state | subscription: subscription}} - {:error, reason} -> {:stop, reason, state} + {:noreply, %__MODULE__{state | subscription: subscription, timer: timer}} + + {:error, reason} -> + {:stop, reason, state} end end @@ -71,24 +72,28 @@ defmodule Indexer.Block.Realtime.Fetcher do block_fetcher: %Block.Fetcher{} = block_fetcher, subscription: %Subscription{} = subscription, previous_number: previous_number, - max_number_seen: max_number_seen + max_number_seen: max_number_seen, + timer: timer } = state ) when is_binary(quantity) do number = quantity_to_integer(quantity) - # Subscriptions don't support getting all the blocks and transactions data, # so we need to go back and get the full block start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen) new_max_number = new_max_number(number, max_number_seen) + :timer.cancel(timer) + new_timer = schedule_polling() + {:noreply, - %{ - state - | previous_number: number, - max_number_seen: new_max_number - }} + %{ + state + | previous_number: number, + max_number_seen: new_max_number, + timer: new_timer + }} end @impl GenServer @@ -102,21 +107,23 @@ defmodule Indexer.Block.Realtime.Fetcher do ) do {number, new_max_number} = case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do - {:ok, number} when number > max_number_seen -> - start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen) + {:ok, number} when is_nil(max_number_seen) or number > max_number_seen -> + start_fetch_and_import(number, block_fetcher, previous_number, number) {max_number_seen, number} - {:error, _} -> + + _ -> {previous_number, max_number_seen} end - schedule_polling() + timer = schedule_polling() {:noreply, %{ state | previous_number: number, - max_number_seen: new_max_number + max_number_seen: new_max_number, + timer: timer }} end @@ -125,7 +132,13 @@ defmodule Indexer.Block.Realtime.Fetcher do defp new_max_number(number, max_number_seen), do: max(number, max_number_seen) defp schedule_polling do - Process.send_after(self(), :poll_latest_block_number, @polling_period) + polling_period = + case AverageBlockTime.average_block_time() do + {:error, :disabled} -> 2_000 + block_time -> round(Duration.to_milliseconds(block_time) * 2) + end + + Process.send_after(self(), :poll_latest_block_number, polling_period) end @import_options ~w(address_hash_to_fetched_balance_block_number)a From 6bfc9cf4a69120db543c7a297787cb99cfcb70da Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Fri, 11 Jan 2019 16:07:14 -0500 Subject: [PATCH 3/4] fixup! fixup! fix: poll on import completion and only new blocks --- apps/indexer/lib/indexer/block/realtime/fetcher.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 16bef60381..902a523af0 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -88,12 +88,12 @@ defmodule Indexer.Block.Realtime.Fetcher do new_timer = schedule_polling() {:noreply, - %{ - state - | previous_number: number, - max_number_seen: new_max_number, - timer: new_timer - }} + %{ + state + | previous_number: number, + max_number_seen: new_max_number, + timer: new_timer + }} end @impl GenServer From 25598e4b7b15614efdbe8ba9f7f2cc046ee3cf12 Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Mon, 14 Jan 2019 09:46:53 -0600 Subject: [PATCH 4/4] Document Explorer.Chain.SmartContract Fixes #1342 Document `t:Explorer.Chain.SmartContract.t/0` to clarify difference between `t:Explorer.Chain.SmartContract.t/0` `contract_source_code` and `t:Explorer.Chain.Address.t/0` `contract_code`. Include Solidity ABI Spec docs so domain knowledge is available with the source. --- apps/explorer/lib/explorer/chain/address.ex | 4 +- .../lib/explorer/chain/smart_contract.ex | 179 +++++++++++++++++- 2 files changed, 181 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 845b17f981..d422faf7b7 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -22,7 +22,9 @@ defmodule Explorer.Chain.Address do * `fetched_coin_balance_block_number` - the `t:Explorer.Chain.Block.t/0` `t:Explorer.Chain.Block.block_number/0` for which `fetched_coin_balance` was fetched * `hash` - the hash of the address's public key - * `contract_code` - the code of the contract when an Address is a contract + * `contract_code` - the binary code of the contract when an Address is a contract. The human-readable + Solidity source code is in `smart_contract` `t:Explorer.Chain.SmartContract.t/0` `contract_source_code` *if* the + contract has been verified * `names` - names known for the address * `inserted_at` - when this address was inserted * `updated_at` when this address was last updated diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 07d51b2216..7e78c7aa17 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -12,12 +12,189 @@ defmodule Explorer.Chain.SmartContract do use Explorer.Schema + @typedoc """ + The name of a parameter to a function or event. + """ + @type parameter_name :: String.t() + + @typedoc """ + Canonical Input or output [type](https://solidity.readthedocs.io/en/develop/abi-spec.html#types). + + * `"address"` - equivalent to `uint160`, except for the assumed interpretation and language typing. For computing the + function selector, `address` is used. + * `"bool"` - equivalent to uint8 restricted to the values 0 and 1. For computing the function selector, bool is used. + * `bytes`: dynamic sized byte sequence + * `"bytes"` - binary type of `M` bytes, `0 < M <= 32`. + * `"fixed"` - synonym for `"fixed128x18". For computing the function selection, `"fixed128x8"` has to be used. + * `"fixedx"` - signed fixed-point decimal number of `M` bits, `8 <= M <= 256`, `M % 8 ==0`, and `0 < N <= 80`, + which denotes the value `v` as `v / (10 ** N)`. + * `"function" - an address (`20` bytes) followed by a function selector (`4` bytes). Encoded identical to `bytes24`. + * `"int"` - synonym for `"int256"`. For computing the function selector `"int256"` has to be used. + * `"int"` - two’s complement signed integer type of `M` bits, `0 < M <= 256`, `M % 8 == 0`. + * `"string"` - dynamic sized unicode string assumed to be UTF-8 encoded. + * `"tuple"` - a tuple. + * `"(,,...,)"` - tuple consisting of the `t:type/0`s ``, …, `Tn`, `n >= 0`. + * `"[]"` - a variable-length array of elements of the given `type`. + * `"[M]"` - a fixed-length array of `M` elements, `M >= 0`, of the given `t:type/0`. + * `"ufixed"` - synonym for `"ufixed128x18". For computing the function selection, `"ufixed128x8"` has to be used. + * `"ufixedx"` - unsigned variant of `"fixedx"` + * `"uint"` - synonym for `"uint256"`. For computing the function selector `"uint256"` has to be used. + * `"uint"` - unsigned integer type of `M` bits, `0 < M <= 256`, `M % 8 == 0.` e.g. `uint32`, `uint8`, `uint256`. + """ + @type type :: String.t() + + @typedoc """ + Name of component. + """ + @type component_name :: String.t() + + @typedoc """ + A component of a [tuple](https://solidity.readthedocs.io/en/develop/abi-spec.html#handling-tuple-types). + + * `"name"` - name of the component. + * `"type"` - `t:type/0`. + """ + @type component :: %{String.t() => component_name() | type()} + + @typedoc """ + The components of a [tuple](https://solidity.readthedocs.io/en/develop/abi-spec.html#handling-tuple-types). + """ + @type components :: [component()] + + @typedoc """ + * `"event"` + """ + @type event_type :: String.t() + + @typedoc """ + Name of an event in an `t:abi/0`. + """ + @type event_name :: String.t() + + @typedoc """ + * `true` - if field is part of the `t:Explorer.Chain.Log.t/0` `topics`. + * `false` - if field is part of the `t:Explorer.Chain.Log.t/0` `data`. + """ + @type indexed :: boolean() + + @typedoc """ + * `"name"` - `t:parameter_name/0`. + * `"type"` - `t:type/0`. + * `"components" `- `t:components/0` used when `"type"` is a tuple type. + * `"indexed"` - `t:indexed/0`. + """ + @type event_input :: %{String.t() => parameter_name() | type() | components() | indexed()} + + @typedoc """ + * `true` - event was declared as `anonymous`. + * `false` - otherwise. + """ + @type anonymous :: boolean() + + @typedoc """ + * `"type" - `t:event_type/0` + * `"name"` - `t:event_name/0` + * `"inputs"` - `t:list/0` of `t:event_input/0`. + * `"anonymous"` - t:anonymous/0` + """ + @type event_description :: %{String.t() => term()} + + @typedoc """ + * `"function"` + * `"constructor"` + * `"fallback"` - the default, unnamed function + """ + @type function_type :: String.t() + + @typedoc """ + Name of a function in an `t:abi/0`. + """ + @type function_name :: String.t() + + @typedoc """ + * `"name"` - t:parameter_name/0`. + * `"type"` - `t:type/0`. + * `"components"` - `t:components/0` used when `"type"` is a tuple type. + """ + @type function_input :: %{String.t() => parameter_name() | type() | components()} + + @typedoc """ + * `"type" - `t:type/0` + """ + @type function_output :: %{String.t() => type()} + + @typedoc """ + * `"pure"` - [specified to not read blockchain state](https://solidity.readthedocs.io/en/develop/contracts.html#pure-functions). + * `"view"` - [specified to not modify the blockchain state](https://solidity.readthedocs.io/en/develop/contracts.html#view-functions). + * `"nonpayable"` - function does not accept Ether. + **NOTE**: Sending non-zero Ether to non-payable function will revert the transaction. + * `"payable"` - function accepts Ether. + """ + @type state_mutability :: String.t() + + @typedoc """ + **Deprecated:** Use `t:function_description/0` `"stateMutability"`: + + * `true` - `"payable"` + * `false` - `"pure"`, `"view"`, or `"nonpayable"`. + """ + @type payable :: boolean() + + @typedoc """ + **Deprecated:** Use `t:function_description/0` `"stateMutability"`: + + * `true` - `"pure"` or `"view"`. + * `false` - `"nonpayable"` or `"payable"`. + """ + @type constant :: boolean() + + @typedoc """ + The [function description](https://solidity.readthedocs.io/en/develop/abi-spec.html#json) for a function in the + `t:abi.t/0`. + + * `"type"` - `t:function_type/0` + * `"name" - `t:function_name/0` + * `"inputs` - `t:list/0` of `t:function_input/0`. + * `"outputs" - `t:list/0` of `t:output/0`. + * `"stateMutability"` - `t:state_mutability/0` + * `"payable"` - `t:payable/0`. + **WARNING:** Deprecated and will be removed in the future. Use `"stateMutability"` instead. + * `"constant"` - `t:constant/0`. + **WARNING:** Deprecated and will be removed in the future. Use `"stateMutability"` instead. + """ + @type function_description :: %{ + String.t() => + function_type() + | function_name() + | [function_input()] + | [function_output()] + | state_mutability() + | payable() + | constant() + } + + @typedoc """ + The [JSON ABI specification](https://solidity.readthedocs.io/en/develop/abi-spec.html#json) for a contract. + """ + @type abi :: [event_description | function_description] + + @typedoc """ + * `name` - the human-readable name of the smart contract. + * `compiler_version` - the version of the Solidity compiler used to compile `contract_source_code` with `optimization` + into `address` `t:Explorer.Chain.Address.t/0` `contract_code`. + * `optimization` - whether optimizations were turned on when compiling `contract_source_code` into `address` + `t:Explorer.Chain.Address.t/0` `contract_code`. + * `contract_source_code` - the Solidity source code that was compiled by `compiler_version` with `optimization` to + produce `address` `t:Explorer.Chain.Address.t/0` `contract_code`. + * `abi` - The [JSON ABI specification](https://solidity.readthedocs.io/en/develop/abi-spec.html#json) for this + contract. + """ @type t :: %Explorer.Chain.SmartContract{ name: String.t(), compiler_version: String.t(), optimization: boolean, contract_source_code: String.t(), - abi: {:array, :map} + abi: [function_description] } schema "smart_contracts" do