Merge branch 'master' into feature/#1476-add-styles-for-POSDAO-network

* master:
  Update changelog
  Address review comments
  Add a migration to mark all invalid blocks as non-consensus (#1644)
  Force consensus loss for parent block if its hash mismatches parent_hash
  Add regression test for #1644
  feat: add listcontracts endpoint
  fix view test
  use correct type for evm_version
  define evm versions in one place
  fix dialyzer
  set only hours in env var
  fetch transaction period from env variables
  add changelog entry
  add petersburg evm version
  add CHANGELOG entry
  fix credo
  fix tests
  use cache for estimated transaction count
  add transaction count cache
pull/1704/head
Gabriel Rodriguez Alsina 6 years ago
commit 3607f533e3
  1. 17
      CHANGELOG.md
  2. 18
      apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_controller.ex
  3. 23
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  4. 91
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex
  5. 42
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/helpers.ex
  6. 117
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  7. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex
  8. 64
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
  9. 31
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  10. 192
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs
  11. 3
      apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs
  12. 8
      apps/block_scout_web/test/block_scout_web/features/pages/contract_verify_page.ex
  13. 5
      apps/explorer/lib/explorer/application.ex
  14. 89
      apps/explorer/lib/explorer/chain.ex
  15. 15
      apps/explorer/lib/explorer/chain/address.ex
  16. 48
      apps/explorer/lib/explorer/chain/import/runner/blocks.ex
  17. 13
      apps/explorer/lib/explorer/chain/smart_contract.ex
  18. 154
      apps/explorer/lib/explorer/chain/transaction_count_cache.ex
  19. 4
      apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex
  20. 6
      apps/explorer/priv/repo/migrations/scripts/20190326202921_lose_consensus_for_invalid_blocks.sql
  21. 31
      apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
  22. 54
      apps/explorer/test/explorer/chain/transaction_count_cache_test.exs

@ -2,8 +2,18 @@
### Features
- [1611](https://github.com/poanetwork/blockscout/pull/1611) - allow setting the first indexing block
- [1596](https://github.com/poanetwork/blockscout/pull/1596) - add endpoint to create decompiled contracts
### Fixes
### Chore
## 1.3.8-beta
### Features
- [#1611](https://github.com/poanetwork/blockscout/pull/1611) - allow setting the first indexing block
- [#1596](https://github.com/poanetwork/blockscout/pull/1596) - add endpoint to create decompiled contracts
- [#1634](https://github.com/poanetwork/blockscout/pull/1634) - add transaction count cache
### Fixes
@ -14,6 +24,8 @@
- [#1639](https://github.com/poanetwork/blockscout/pull/1614) - Optimize token holder count updates when importing address current balances
- [#1643](https://github.com/poanetwork/blockscout/pull/1643) - Set internal_transactions_indexed_at for empty blocks
- [#1647](https://github.com/poanetwork/blockscout/pull/1647) - Fix typo in view
- [#1650](https://github.com/poanetwork/blockscout/pull/1650) - Add petersburg evm version to smart contract verifier
- [#1657](https://github.com/poanetwork/blockscout/pull/1657) - Force consensus loss for parent block if its hash mismatches parent_hash
### Chore
@ -40,6 +52,7 @@
- [#1589](https://github.com/poanetwork/blockscout/pull/1589) - RPC endpoint to list addresses
- [#1567](https://github.com/poanetwork/blockscout/pull/1567) - Allow setting different configuration just for realtime fetcher
- [#1562](https://github.com/poanetwork/blockscout/pull/1562) - Add incoming transactions count to contract view
- [#1608](https://github.com/poanetwork/blockscout/pull/1608) - Add listcontracts RPC Endpoint
### Fixes

@ -2,9 +2,7 @@ defmodule BlockScoutWeb.AddressContractVerificationController do
use BlockScoutWeb, :controller
alias Explorer.Chain.SmartContract
alias Explorer.SmartContract.{Publisher, Solidity.CompilerVersion}
@evm_versions ["homestead", "tangerineWhistle", "spuriousDragon", "byzantium", "constantinople"]
alias Explorer.SmartContract.{Publisher, Solidity.CodeCompiler, Solidity.CompilerVersion}
def new(conn, %{"address_id" => address_hash_string}) do
changeset =
@ -15,7 +13,11 @@ defmodule BlockScoutWeb.AddressContractVerificationController do
{:ok, compiler_versions} = CompilerVersion.fetch_versions()
render(conn, "new.html", changeset: changeset, compiler_versions: compiler_versions, evm_versions: @evm_versions)
render(conn, "new.html",
changeset: changeset,
compiler_versions: compiler_versions,
evm_versions: CodeCompiler.allowed_evm_versions()
)
end
def create(
@ -27,7 +29,7 @@ defmodule BlockScoutWeb.AddressContractVerificationController do
"evm_version" => evm_version
}
) do
smart_sontact_with_evm_version = Map.put(smart_contract, "evm_version", evm_version)
smart_sontact_with_evm_version = Map.put(smart_contract, "evm_version", evm_version["evm_version"])
case Publisher.publish(address_hash_string, smart_sontact_with_evm_version, external_libraries) do
{:ok, _smart_contract} ->
@ -36,7 +38,11 @@ defmodule BlockScoutWeb.AddressContractVerificationController do
{:error, changeset} ->
{:ok, compiler_versions} = CompilerVersion.fetch_versions()
render(conn, "new.html", changeset: changeset, compiler_versions: compiler_versions, evm_versions: @evm_versions)
render(conn, "new.html",
changeset: changeset,
compiler_versions: compiler_versions,
evm_versions: CodeCompiler.allowed_evm_versions()
)
end
end
end

@ -1,6 +1,7 @@
defmodule BlockScoutWeb.API.RPC.AddressController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.API.RPC.Helpers
alias Explorer.{Chain, Etherscan}
alias Explorer.Chain.{Address, Wei}
@ -162,7 +163,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
def getminedblocks(conn, params) do
options = put_pagination_options(%{}, params)
options = Helpers.put_pagination_options(%{}, params)
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
@ -188,7 +189,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
def optional_params(params) do
%{}
|> put_order_by_direction(params)
|> put_pagination_options(params)
|> Helpers.put_pagination_options(params)
|> put_start_block(params)
|> put_end_block(params)
|> put_filter_by(params)
@ -338,24 +339,6 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
end
defp put_pagination_options(options, params) do
with %{"page" => page, "offset" => offset} <- params,
{page_number, ""} when page_number > 0 <- Integer.parse(page),
{page_size, ""} when page_size > 0 <- Integer.parse(offset),
:ok <- validate_max_page_size(page_size) do
options
|> Map.put(:page_number, page_number)
|> Map.put(:page_size, page_size)
else
_ ->
options
end
end
defp validate_max_page_size(page_size) do
if page_size <= Etherscan.page_size_max(), do: :ok, else: :error
end
defp put_start_block(options, params) do
with %{"startblock" => startblock_param} <- params,
{start_block, ""} <- Integer.parse(startblock_param) do

@ -1,7 +1,30 @@
defmodule BlockScoutWeb.API.RPC.ContractController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.API.RPC.Helpers
alias Explorer.Chain
alias Explorer.Chain.SmartContract
def listcontracts(conn, params) do
with pagination_options <- Helpers.put_pagination_options(%{}, params),
{:params, {:ok, options}} <- {:params, add_filter(pagination_options, params)} do
options_with_defaults =
options
|> Map.put_new(:page_number, 0)
|> Map.put_new(:page_size, 10)
contracts = list_contracts(options_with_defaults)
conn
|> put_status(200)
|> render(:listcontracts, %{contracts: contracts})
else
{:params, {:error, error}} ->
conn
|> put_status(400)
|> render(:error, error: error)
end
end
def getabi(conn, params) do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
@ -24,7 +47,10 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
{:contract, {:ok, contract}} <- to_smart_contract(address_hash) do
render(conn, :getsourcecode, %{contract: contract})
render(conn, :getsourcecode, %{
contract: contract,
address_hash: address_hash
})
else
{:address_param, :error} ->
render(conn, :error, error: "Query parameter address is required")
@ -33,10 +59,64 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
render(conn, :error, error: "Invalid address hash")
{:contract, :not_found} ->
render(conn, :getsourcecode, %{contract: nil})
render(conn, :getsourcecode, %{contract: nil, address_hash: nil})
end
end
defp list_contracts(%{page_number: page_number, page_size: page_size} = opts) do
offset = (max(page_number, 1) - 1) * page_size
case Map.get(opts, :filter) do
:verified ->
Chain.list_verified_contracts(page_size, offset)
:decompiled ->
Chain.list_decompiled_contracts(page_size, offset)
:unverified ->
Chain.list_unverified_contracts(page_size, offset)
:not_decompiled ->
Chain.list_not_decompiled_contracts(page_size, offset)
_ ->
Chain.list_contracts(page_size, offset)
end
end
defp add_filter(options, params) do
with {:param, {:ok, value}} <- {:param, Map.fetch(params, "filter")},
{:validation, {:ok, filter}} <- {:validation, contracts_filter(value)} do
{:ok, Map.put(options, :filter, filter)}
else
{:param, :error} -> {:ok, options}
{:validation, {:error, error}} -> {:error, error}
end
end
defp contracts_filter(nil), do: {:ok, nil}
defp contracts_filter(1), do: {:ok, :verified}
defp contracts_filter(2), do: {:ok, :decompiled}
defp contracts_filter(3), do: {:ok, :unverified}
defp contracts_filter(4), do: {:ok, :not_decompiled}
defp contracts_filter("verified"), do: {:ok, :verified}
defp contracts_filter("decompiled"), do: {:ok, :decompiled}
defp contracts_filter("unverified"), do: {:ok, :unverified}
defp contracts_filter("not_decompiled"), do: {:ok, :not_decompiled}
defp contracts_filter(filter) when is_bitstring(filter) do
case Integer.parse(filter) do
{number, ""} -> contracts_filter(number)
_ -> {:error, contracts_filter_error_message(filter)}
end
end
defp contracts_filter(filter), do: {:error, contracts_filter_error_message(filter)}
defp contracts_filter_error_message(filter) do
"#{filter} is not a valid value for `filter`. Please use one of: verified, decompiled, unverified, not_decompiled, 1, 2, 3, 4."
end
defp fetch_address(params) do
{:address_param, Map.fetch(params, "address")}
end
@ -48,8 +128,11 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
defp to_smart_contract(address_hash) do
result =
case Chain.address_hash_to_smart_contract(address_hash) do
nil -> :not_found
contract -> {:ok, contract}
nil ->
:not_found
contract ->
{:ok, SmartContract.preload_decompiled_smart_contract(contract)}
end
{:contract, result}

@ -0,0 +1,42 @@
defmodule BlockScoutWeb.API.RPC.Helpers do
@moduledoc """
Small helpers for RPC api controllers.
"""
alias Explorer.Etherscan
def put_pagination_options(options, params) do
options
|> put_page_option(params)
|> put_offset_option(params)
end
def put_page_option(options, %{"page" => page}) do
case Integer.parse(page) do
{page_number, ""} when page_number > 0 ->
Map.put(options, :page_number, page_number)
_ ->
options
end
end
def put_page_option(options, _), do: options
def put_offset_option(options, %{"offset" => offset}) do
with {page_size, ""} when page_size > 0 <- Integer.parse(offset),
:ok <- validate_max_page_size(page_size) do
Map.put(options, :page_size, page_size)
else
_ ->
options
end
end
def put_offset_option(options, _) do
options
end
defp validate_max_page_size(page_size) do
if page_size <= Etherscan.page_size_max(), do: :ok, else: :error
end
end

@ -276,6 +276,45 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil
}
@contract_listcontracts_example_value %{
"status" => "1",
"message" => "OK",
"result" => [
%{
"SourceCode" => """
pragma solidity >0.4.24;
contract Test {
constructor() public { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
""",
"ABI" => """
[{
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]
""",
"ContractName" => "Test",
"CompilerVersion" => "v0.2.1-2016-01-30-91a6b35",
"OptimizationUsed" => "1"
}
]
}
@contract_getabi_example_value %{
"status" => "1",
"message" => "OK",
@ -742,6 +781,7 @@ defmodule BlockScoutWeb.Etherscan do
@contract_model %{
name: "Contract",
fields: %{
"Address" => @address_hash_type,
"SourceCode" => %{
type: "contract source code",
definition: "The contract's source code.",
@ -757,6 +797,33 @@ defmodule BlockScoutWeb.Etherscan do
}"
"""
},
"DecompilerVersion" => %{
type: "decompiler version",
definition: "When decompiled source code is present, the decompiler version with which it was generated.",
example: "decompiler.version"
},
"DecompiledSourceCode" => %{
type: "contract decompiled source code",
definition: "The contract's decompiled source code.",
example: """
const name() = 'CryptoKitties'
const GEN0_STARTING_PRICE() = 10^16
const GEN0_AUCTION_DURATION() = 86400
const GEN0_CREATION_LIMIT() = 45000
const symbol() = 'CK'
const PROMO_CREATION_LIMIT() = 5000
def storage:
ceoAddress is addr # mask(160, 0) at storage #0
cfoAddress is addr # mask(160, 0) at storage #1
stor1.768 is uint16 => uint256 # mask(256, 768) at storage #1
cooAddress is addr # mask(160, 0) at storage #2
stor2.0 is uint256 => uint256 # mask(256, 0) at storage #2
paused is uint8 # mask(8, 160) at storage #2
stor2.256 is uint256 => uint256 # mask(256, 256) at storage #2
stor3 is uint32 #
...<continues>
"""
},
"ABI" => %{
type: "ABI",
definition: "JSON string for the contract's Application Binary Interface (ABI)",
@ -1343,11 +1410,6 @@ defmodule BlockScoutWeb.Etherscan do
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@account_getminedblocks_example_value_error)
}
]
}
@ -1635,6 +1697,50 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@contract_listcontracts_action %{
name: "listcontracts",
description: "Get a list of contracts, sorted ascending by the time they were first seen by the explorer.",
required_params: [],
optional_params: [
%{
key: "page",
type: "integer",
description:
"A nonnegative integer that represents the page number to be used for pagination. 'offset' must be provided in conjunction."
},
%{
key: "offset",
type: "integer",
description:
"A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction."
},
%{
key: "filter",
type: "string",
description:
"verified|decompiled|unverified|not_decompiled, or 1|2|3|4 respectively. This requests only contracts with that status."
}
],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@contract_listcontracts_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "array",
array_type: @contract_model
}
}
}
}
]
}
@contract_getabi_action %{
name: "getabi",
description: "Get ABI for verified contract. Also available through a GraphQL 'addresses' query.",
@ -1862,6 +1968,7 @@ defmodule BlockScoutWeb.Etherscan do
@contract_module %{
name: "contract",
actions: [
@contract_listcontracts_action,
@contract_getabi_action,
@contract_getsourcecode_action
]

@ -29,7 +29,7 @@
<div class="form-group">
<%= label :evm_version, :evm_version, gettext("EVM Version") %>
<%= select :evm_version, :evm_version, @evm_versions, class: "form-control", selected: "byzantium", "aria-describedby": "evm-version-help-block" %>
<%= select :evm_version, :evm_version, @evm_versions, class: "form-control", selected: "petersburg", "aria-describedby": "evm-version-help-block" %>
</div>
<div class="form-group mb-4">

@ -2,40 +2,88 @@ defmodule BlockScoutWeb.API.RPC.ContractView do
use BlockScoutWeb, :view
alias BlockScoutWeb.API.RPC.RPCView
alias Explorer.Chain.{Address, DecompiledSmartContract, SmartContract}
def render("listcontracts.json", %{contracts: contracts}) do
contracts = Enum.map(contracts, &prepare_contract/1)
RPCView.render("show.json", data: contracts)
end
def render("getabi.json", %{abi: abi}) do
RPCView.render("show.json", data: Jason.encode!(abi))
end
def render("getsourcecode.json", %{contract: contract}) do
RPCView.render("show.json", data: prepare_contract(contract))
def render("getsourcecode.json", %{contract: contract, address_hash: address_hash}) do
RPCView.render("show.json", data: [prepare_source_code_contract(contract, address_hash)])
end
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
defp prepare_contract(nil) do
[
defp prepare_source_code_contract(nil, address_hash) do
%{
"Address" => to_string(address_hash),
"SourceCode" => "",
"ABI" => "Contract source code not verified",
"ContractName" => "",
"CompilerVersion" => "",
"DecompiledSourceCode" => "",
"DecompilerVersion" => "",
"OptimizationUsed" => ""
}
]
end
defp prepare_contract(contract) do
[
defp prepare_source_code_contract(contract, _) do
%{
"Address" => to_string(contract.address_hash),
"SourceCode" => contract.contract_source_code,
"ABI" => Jason.encode!(contract.abi),
"ContractName" => contract.name,
"DecompiledSourceCode" => decompiled_source_code(contract.decompiled_smart_contract),
"DecompilerVersion" => decompiler_version(contract.decompiled_smart_contract),
"CompilerVersion" => contract.compiler_version,
"OptimizationUsed" => if(contract.optimization, do: "1", else: "0")
}
]
end
defp prepare_contract(%Address{hash: hash, smart_contract: nil, decompiled_smart_contract: decompiled_smart_contract}) do
%{
"Address" => to_string(hash),
"SourceCode" => "",
"ABI" => "Contract source code not verified",
"ContractName" => "",
"DecompiledSourceCode" => decompiled_source_code(decompiled_smart_contract),
"DecompilerVersion" => decompiler_version(decompiled_smart_contract),
"CompilerVersion" => "",
"OptimizationUsed" => ""
}
end
defp prepare_contract(%Address{
hash: hash,
smart_contract: %SmartContract{} = contract,
decompiled_smart_contract: decompiled_smart_contract
}) do
%{
"Address" => to_string(hash),
"SourceCode" => contract.contract_source_code,
"ABI" => Jason.encode!(contract.abi),
"ContractName" => contract.name,
"DecompiledSourceCode" => decompiled_source_code(decompiled_smart_contract),
"DecompilerVersion" => decompiler_version(decompiled_smart_contract),
"CompilerVersion" => contract.compiler_version,
"OptimizationUsed" => if(contract.optimization, do: "1", else: "0")
}
end
defp decompiled_source_code(nil), do: "Contract source code not decompiled."
defp decompiled_source_code(%DecompiledSmartContract{decompiled_source_code: decompiled_source_code}) do
decompiled_source_code
end
defp decompiler_version(nil), do: ""
defp decompiler_version(%DecompiledSmartContract{decompiler_version: decompiler_version}), do: decompiler_version
end

@ -767,34 +767,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
assert response["message"] == "OK"
end
test "ignores pagination params if page is less than 1", %{conn: conn} do
address = insert(:address)
6
|> insert_list(:transaction, from_address: address)
|> with_block()
params = %{
"module" => "account",
"action" => "txlist",
"address" => "#{address.hash}",
# page number
"page" => "0",
# page size
"offset" => "2"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert length(response["result"]) == 6
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "ignores pagination params if offset is less than 1", %{conn: conn} do
test "ignores offset param if offset is less than 1", %{conn: conn} do
address = insert(:address)
6
@ -821,7 +794,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
assert response["message"] == "OK"
end
test "ignores pagination params if offset is over 10,000", %{conn: conn} do
test "ignores offset param if offset is over 10,000", %{conn: conn} do
address = insert(:address)
6

@ -1,6 +1,190 @@
defmodule BlockScoutWeb.API.RPC.ContractControllerTest do
use BlockScoutWeb.ConnCase
describe "listcontracts" do
setup do
%{params: %{"module" => "contract", "action" => "listcontracts"}}
end
test "with an invalid filter value", %{conn: conn, params: params} do
response =
conn
|> get("/api", Map.put(params, "filter", "invalid"))
|> json_response(400)
assert response["message"] ==
"invalid is not a valid value for `filter`. Please use one of: verified, decompiled, unverified, not_decompiled, 1, 2, 3, 4."
assert response["status"] == "0"
end
test "with no contracts", %{conn: conn, params: params} do
response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == []
end
test "with a verified smart contract, all contract information is shown", %{conn: conn, params: params} do
contract = insert(:smart_contract)
response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == [
%{
"ABI" => Jason.encode!(contract.abi),
"Address" => to_string(contract.address_hash),
"CompilerVersion" => contract.compiler_version,
"ContractName" => contract.name,
"DecompiledSourceCode" => "Contract source code not decompiled.",
"DecompilerVersion" => "",
"OptimizationUsed" => if(contract.optimization, do: "1", else: "0"),
"SourceCode" => contract.contract_source_code
}
]
end
test "with an unverified contract address, only basic information is shown", %{conn: conn, params: params} do
address = insert(:contract_address)
response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == [
%{
"ABI" => "Contract source code not verified",
"Address" => to_string(address.hash),
"CompilerVersion" => "",
"ContractName" => "",
"DecompiledSourceCode" => "Contract source code not decompiled.",
"DecompilerVersion" => "",
"OptimizationUsed" => "",
"SourceCode" => ""
}
]
end
test "filtering for only unverified contracts shows only unverified contracts", %{params: params, conn: conn} do
address = insert(:contract_address)
insert(:smart_contract)
response =
conn
|> get("/api", Map.put(params, "filter", "unverified"))
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == [
%{
"ABI" => "Contract source code not verified",
"Address" => to_string(address.hash),
"CompilerVersion" => "",
"ContractName" => "",
"DecompiledSourceCode" => "Contract source code not decompiled.",
"DecompilerVersion" => "",
"OptimizationUsed" => "",
"SourceCode" => ""
}
]
end
test "filtering for only verified contracts shows only verified contracts", %{params: params, conn: conn} do
insert(:contract_address)
contract = insert(:smart_contract)
response =
conn
|> get("/api", Map.put(params, "filter", "verified"))
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == [
%{
"ABI" => Jason.encode!(contract.abi),
"Address" => to_string(contract.address_hash),
"CompilerVersion" => contract.compiler_version,
"DecompiledSourceCode" => "Contract source code not decompiled.",
"DecompilerVersion" => "",
"ContractName" => contract.name,
"OptimizationUsed" => if(contract.optimization, do: "1", else: "0"),
"SourceCode" => contract.contract_source_code
}
]
end
test "filtering for only decompiled contracts shows only decompiled contracts", %{params: params, conn: conn} do
insert(:contract_address)
decompiled_smart_contract = insert(:decompiled_smart_contract)
response =
conn
|> get("/api", Map.put(params, "filter", "decompiled"))
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == [
%{
"ABI" => "Contract source code not verified",
"Address" => to_string(decompiled_smart_contract.address_hash),
"CompilerVersion" => "",
"ContractName" => "",
"DecompiledSourceCode" => decompiled_smart_contract.decompiled_source_code,
"DecompilerVersion" => "test_decompiler",
"OptimizationUsed" => "",
"SourceCode" => ""
}
]
end
test "filtering for only not_decompiled (and by extension not verified contracts)", %{params: params, conn: conn} do
insert(:decompiled_smart_contract)
insert(:smart_contract)
contract_address = insert(:contract_address)
response =
conn
|> get("/api", Map.put(params, "filter", "not_decompiled"))
|> json_response(200)
assert response["message"] == "OK"
assert response["status"] == "1"
assert response["result"] == [
%{
"ABI" => "Contract source code not verified",
"Address" => to_string(contract_address.hash),
"CompilerVersion" => "",
"ContractName" => "",
"DecompiledSourceCode" => "Contract source code not decompiled.",
"DecompilerVersion" => "",
"OptimizationUsed" => "",
"SourceCode" => ""
}
]
end
end
describe "getabi" do
test "with missing address hash", %{conn: conn} do
params = %{
@ -119,11 +303,14 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do
expected_result = [
%{
"Address" => "",
"SourceCode" => "",
"ABI" => "Contract source code not verified",
"ContractName" => "",
"CompilerVersion" => "",
"OptimizationUsed" => ""
"OptimizationUsed" => "",
"DecompiledSourceCode" => "",
"DecompilerVersion" => ""
}
]
@ -148,13 +335,16 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do
expected_result = [
%{
"Address" => to_string(contract.address_hash),
"SourceCode" => contract.contract_source_code,
"ABI" => Jason.encode!(contract.abi),
"ContractName" => contract.name,
"CompilerVersion" => contract.compiler_version,
"DecompiledSourceCode" => "Contract source code not decompiled.",
# The contract's optimization value is true, so the expected value
# for `OptimizationUsed` is "1". If it was false, the expected value
# would be "0".
"DecompilerVersion" => "",
"OptimizationUsed" => "1"
}
]

@ -36,7 +36,8 @@ defmodule BlockScoutWeb.AddressContractVerificationTest do
contract_name: name,
version: version,
optimization: false,
source_code: source_code
source_code: source_code,
evm_version: "byzantium"
})
|> ContractVerifyPage.verify_and_publish()

@ -13,7 +13,8 @@ defmodule BlockScoutWeb.ContractVerifyPage do
contract_name: contract_name,
version: version,
optimization: optimization,
source_code: source_code
source_code: source_code,
evm_version: evm_version
}) do
session
|> fill_in(css("[data-test='contract_name']"), with: contract_name)
@ -24,6 +25,11 @@ defmodule BlockScoutWeb.ContractVerifyPage do
_ -> click(session, option(version))
end
case evm_version do
nil -> nil
_ -> click(session, option(evm_version))
end
case optimization do
true ->
click(session, radio_button("Yes"))

@ -6,7 +6,7 @@ defmodule Explorer.Application do
use Application
alias Explorer.Admin
alias Explorer.Chain.BlockNumberCache
alias Explorer.Chain.{BlockNumberCache, TransactionCountCache}
alias Explorer.Repo.PrometheusLogger
@impl Application
@ -27,7 +27,8 @@ defmodule Explorer.Application do
Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor),
Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor),
{Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents},
{Admin.Recovery, [[], [name: Admin.Recovery]]}
{Admin.Recovery, [[], [name: Admin.Recovery]]},
{TransactionCountCache, [[], []]}
]
children = base_children ++ configurable_children()

@ -20,7 +20,6 @@ defmodule Explorer.Chain do
import EthereumJSONRPC, only: [integer_to_quantity: 1]
alias Ecto.Adapters.SQL
alias Ecto.{Changeset, Multi}
alias Explorer.Chain.{
@ -40,6 +39,7 @@ defmodule Explorer.Chain do
Token,
TokenTransfer,
Transaction,
TransactionCountCache,
Wei
}
@ -1868,10 +1868,7 @@ defmodule Explorer.Chain do
"""
@spec transaction_estimated_count() :: non_neg_integer()
def transaction_estimated_count do
%Postgrex.Result{rows: [[rows]]} =
SQL.query!(Repo, "SELECT reltuples::BIGINT AS estimate FROM pg_class WHERE relname='transactions'")
rows
TransactionCountCache.value()
end
@doc """
@ -2629,6 +2626,88 @@ defmodule Explorer.Chain do
Repo.all(query, timeout: :infinity)
end
def list_decompiled_contracts(limit, offset) do
query =
from(
address in Address,
join: decompiled_smart_contract in DecompiledSmartContract,
on: decompiled_smart_contract.address_hash == address.hash,
preload: [{:decompiled_smart_contract, decompiled_smart_contract}, :smart_contract],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
def list_verified_contracts(limit, offset) do
query =
from(
address in Address,
where: not is_nil(address.contract_code),
join: smart_contract in SmartContract,
on: smart_contract.address_hash == address.hash,
preload: [{:smart_contract, smart_contract}, :decompiled_smart_contract],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
def list_contracts(limit, offset) do
query =
from(
address in Address,
where: not is_nil(address.contract_code),
preload: [:smart_contract, :decompiled_smart_contract],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
def list_unverified_contracts(limit, offset) do
query =
from(
address in Address,
left_join: smart_contract in SmartContract,
on: smart_contract.address_hash == address.hash,
where: not is_nil(address.contract_code),
where: is_nil(smart_contract.address_hash),
preload: [{:smart_contract, smart_contract}, :decompiled_smart_contract],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
def list_not_decompiled_contracts(limit, offset) do
query =
from(
address in Address,
left_join: smart_contract in SmartContract,
on: smart_contract.address_hash == address.hash,
left_join: decompiled_smart_contract in DecompiledSmartContract,
on: decompiled_smart_contract.address_hash == address.hash,
preload: [smart_contract: smart_contract, decompiled_smart_contract: decompiled_smart_contract],
where: not is_nil(address.contract_code),
where: is_nil(smart_contract.address_hash),
where: is_nil(decompiled_smart_contract.address_hash),
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
@doc """
Combined block reward from all the fees.
"""

@ -8,7 +8,19 @@ defmodule Explorer.Chain.Address do
use Explorer.Schema
alias Ecto.Changeset
alias Explorer.Chain.{Address, Block, Data, Hash, InternalTransaction, SmartContract, Token, Transaction, Wei}
alias Explorer.Chain.{
Address,
Block,
Data,
DecompiledSmartContract,
Hash,
InternalTransaction,
SmartContract,
Token,
Transaction,
Wei
}
@optional_attrs ~w(contract_code fetched_coin_balance fetched_coin_balance_block_number nonce)a
@required_attrs ~w(hash)a
@ -64,6 +76,7 @@ defmodule Explorer.Chain.Address do
field(:nonce, :integer)
has_one(:smart_contract, SmartContract)
has_one(:decompiled_smart_contract, DecompiledSmartContract)
has_one(:token, Token, foreign_key: :contract_address_hash)
has_one(

@ -46,6 +46,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
|> Map.put(:timestamps, timestamps)
ordered_consensus_block_numbers = ordered_consensus_block_numbers(changes_list)
where_invalid_parent = where_invalid_parent(changes_list)
where_forked = where_forked(changes_list)
multi
@ -69,6 +70,9 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
|> Multi.run(:lose_consensus, fn repo, _ ->
lose_consensus(repo, ordered_consensus_block_numbers, insert_options)
end)
|> Multi.run(:lose_invalid_parent_consensus, fn repo, _ ->
lose_invalid_parent_consensus(repo, where_invalid_parent, insert_options)
end)
|> Multi.run(:delete_address_token_balances, fn repo, _ ->
delete_address_token_balances(repo, ordered_consensus_block_numbers, insert_options)
end)
@ -312,6 +316,32 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
end
end
defp lose_invalid_parent_consensus(repo, where_invalid_parent, %{
timeout: timeout,
timestamps: %{updated_at: updated_at}
}) do
query =
from(
block in where_invalid_parent,
update: [
set: [
consensus: false,
updated_at: ^updated_at
]
],
select: [:hash, :number]
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, where_invalid_parent: where_invalid_parent}}
end
end
defp delete_address_token_balances(_, [], _), do: {:ok, []}
defp delete_address_token_balances(repo, ordered_consensus_block_numbers, %{timeout: timeout}) do
@ -543,12 +573,22 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
initial = from(t in Transaction, where: false)
Enum.reduce(blocks_changes, initial, fn %{consensus: consensus, hash: hash, number: number}, acc ->
case consensus do
false ->
if consensus do
from(transaction in acc, or_where: transaction.block_hash != ^hash and transaction.block_number == ^number)
else
from(transaction in acc, or_where: transaction.block_hash == ^hash and transaction.block_number == ^number)
end
end)
end
true ->
from(transaction in acc, or_where: transaction.block_hash != ^hash and transaction.block_number == ^number)
defp where_invalid_parent(blocks_changes) when is_list(blocks_changes) do
initial = from(b in Block, where: false)
Enum.reduce(blocks_changes, initial, fn %{consensus: consensus, parent_hash: parent_hash, number: number}, acc ->
if consensus do
from(block in acc, or_where: block.number == ^(number - 1) and block.hash != ^parent_hash)
else
acc
end
end)
end

@ -12,7 +12,8 @@ defmodule Explorer.Chain.SmartContract do
use Explorer.Schema
alias Explorer.Chain.{Address, ContractMethod, Hash}
alias Explorer.Chain.{Address, ContractMethod, DecompiledSmartContract, Hash}
alias Explorer.Repo
@typedoc """
The name of a parameter to a function or event.
@ -208,6 +209,12 @@ defmodule Explorer.Chain.SmartContract do
field(:constructor_arguments, :string)
field(:abi, {:array, :map})
has_one(
:decompiled_smart_contract,
DecompiledSmartContract,
foreign_key: :address_hash
)
belongs_to(
:address,
Address,
@ -219,6 +226,10 @@ defmodule Explorer.Chain.SmartContract do
timestamps()
end
def preload_decompiled_smart_contract(contract) do
Repo.preload(contract, :decompiled_smart_contract)
end
def changeset(%__MODULE__{} = smart_contract, attrs) do
smart_contract
|> cast(attrs, [

@ -0,0 +1,154 @@
defmodule Explorer.Chain.TransactionCountCache do
@moduledoc """
Cache for estimated transaction count.
"""
require Logger
use GenServer
alias Explorer.Chain.Transaction
alias Explorer.Repo
# 2 hours
@cache_period 1_000 * 60 * 60 * 2
@default_value 0
@key "count"
@name __MODULE__
def start_link([params, gen_server_options]) do
name = gen_server_options[:name] || @name
params_with_name = Keyword.put(params, :name, name)
GenServer.start_link(__MODULE__, params_with_name, name: name)
end
def init(params) do
cache_period = period_from_env_var() || params[:cache_period] || @cache_period
current_value = params[:default_value] || @default_value
name = params[:name]
init_ets_table(name)
schedule_cache_update()
{:ok, {{cache_period, current_value, name}, nil}}
end
def value(process_name \\ __MODULE__) do
GenServer.call(process_name, :value)
end
def handle_call(:value, _, {{cache_period, default_value, name}, task}) do
{value, task} =
case cached_values(name) do
nil ->
{default_value, update_cache(task, name)}
{cached_value, timestamp} ->
task =
if current_time() - timestamp > cache_period do
update_cache(task, name)
end
{cached_value, task}
end
{:reply, value, {{cache_period, default_value, name}, task}}
end
def update_cache(nil, name) do
async_update_cache(name)
end
def update_cache(task, _) do
task
end
def handle_cast({:update_cache, value}, {{cache_period, default_value, name}, _}) do
current_time = current_time()
tuple = {value, current_time}
table_name = table_name(name)
:ets.insert(table_name, {@key, tuple})
{:noreply, {{cache_period, default_value, name}, nil}}
end
def handle_info({:DOWN, _, _, _, _}, {{cache_period, default_value, name}, _}) do
{:noreply, {{cache_period, default_value, name}, nil}}
end
def handle_info(_, {{cache_period, default_value, name}, _}) do
{:noreply, {{cache_period, default_value, name}, nil}}
end
# sobelow_skip ["DOS"]
defp table_name(name) do
name
|> Atom.to_string()
|> Macro.underscore()
|> String.to_atom()
end
def async_update_cache(name) do
Task.async(fn ->
try do
result = Repo.aggregate(Transaction, :count, :hash, timeout: :infinity)
GenServer.cast(name, {:update_cache, result})
rescue
e ->
Logger.debug([
"Coudn't update transaction count test #{inspect(e)}"
])
end
end)
end
defp init_ets_table(name) do
table_name = table_name(name)
if :ets.whereis(table_name) == :undefined do
:ets.new(table_name, [
:set,
:named_table,
:public,
write_concurrency: true
])
end
end
defp cached_values(name) do
table_name = table_name(name)
case :ets.lookup(table_name, @key) do
[{_, cached_values}] -> cached_values
_ -> nil
end
end
defp schedule_cache_update do
Process.send_after(self(), :update_cache, 2_000)
end
defp current_time do
utc_now = DateTime.utc_now()
DateTime.to_unix(utc_now, :millisecond)
end
defp period_from_env_var do
case System.get_env("TXS_COUNT_CACHE_PERIOD") do
value when is_binary(value) ->
case Integer.parse(value) do
{integer, ""} -> integer * 1_000 * 60 * 60
_ -> nil
end
_ ->
nil
end
end
end

@ -4,7 +4,7 @@ defmodule Explorer.SmartContract.Solidity.CodeCompiler do
"""
@new_contract_name "New.sol"
@allowed_evm_versions ["homestead", "tangerineWhistle", "spuriousDragon", "byzantium", "constantinople"]
@allowed_evm_versions ["homestead", "tangerineWhistle", "spuriousDragon", "byzantium", "constantinople", "petersburg"]
@doc """
Compiles a code in the solidity command line.
@ -98,6 +98,8 @@ defmodule Explorer.SmartContract.Solidity.CodeCompiler do
end
end
def allowed_evm_versions, do: @allowed_evm_versions
def get_contract_info(contracts, _) when contracts == %{}, do: {:error, :compilation}
def get_contract_info(contracts, name) do

@ -0,0 +1,6 @@
UPDATE blocks SET consensus = FALSE, updated_at = NOW()
WHERE consensus AND number IN (
SELECT b0.number - 1 FROM "blocks" AS b0
LEFT JOIN "blocks" AS b1 ON (b0."parent_hash" = b1."hash") AND b1."consensus"
WHERE b0."number" > 0 AND b0."consensus" AND b1."hash" IS NULL
);

@ -8,6 +8,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
alias Ecto.Multi
alias Explorer.Chain.Import.Runner.{Blocks, Transaction}
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.Chain
alias Explorer.Repo
describe "run/1" do
@ -258,6 +259,36 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
blocks_update_token_holder_counts: []
}} = run_block_consensus_change(block, true, options)
end
# Regression test for https://github.com/poanetwork/blockscout/issues/1644
test "discards parent block if it isn't related to the current one because of reorg",
%{consensus_block: %Block{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do
old_block = insert(:block, parent_hash: block_hash, number: block_number + 1)
insert(:block, parent_hash: old_block.hash, number: old_block.number + 1)
new_block1 = params_for(:block, parent_hash: block_hash, number: block_number + 1, miner_hash: miner_hash)
new_block2 =
params_for(:block, parent_hash: new_block1.hash, number: new_block1.number + 1, miner_hash: miner_hash)
%Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, new_block2)
changes_list = [block_changes]
Multi.new()
|> Blocks.run(changes_list, options)
|> Repo.transaction()
assert Chain.missing_block_number_ranges(block_number..new_block2.number) == [old_block.number..old_block.number]
%Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, new_block1)
changes_list = [block_changes]
Multi.new()
|> Blocks.run(changes_list, options)
|> Repo.transaction()
assert Chain.missing_block_number_ranges(block_number..new_block2.number) == []
end
end
defp count(schema) do

@ -0,0 +1,54 @@
defmodule Explorer.Chain.TransactionCountCacheTest do
use Explorer.DataCase
alias Explorer.Chain.TransactionCountCache
test "returns default transaction count" do
TransactionCountCache.start_link([[], [name: TestCache]])
result = TransactionCountCache.value(TestCache)
assert result == 0
end
test "updates cache if initial value is zero" do
TransactionCountCache.start_link([[], [name: TestCache]])
insert(:transaction)
insert(:transaction)
_result = TransactionCountCache.value(TestCache)
Process.sleep(1000)
updated_value = TransactionCountCache.value(TestCache)
assert updated_value == 2
end
test "does not update cache if cache period did not pass" do
TransactionCountCache.start_link([[], [name: TestCache]])
insert(:transaction)
insert(:transaction)
_result = TransactionCountCache.value(TestCache)
Process.sleep(1000)
updated_value = TransactionCountCache.value(TestCache)
assert updated_value == 2
insert(:transaction)
insert(:transaction)
_updated_value = TransactionCountCache.value(TestCache)
Process.sleep(1000)
updated_value = TransactionCountCache.value(TestCache)
assert updated_value == 2
end
end
Loading…
Cancel
Save