Merge pull request #1596 from poanetwork/ab-create-decompiled-smart-contracts

create decompiled smart contracts
pull/1647/head
Victor Baranov 6 years ago committed by GitHub
commit 809219de86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 3
      apps/block_scout_web/config/config.exs
  3. 79
      apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex
  4. 2
      apps/block_scout_web/lib/block_scout_web/router.ex
  5. 99
      apps/block_scout_web/test/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller_test.exs
  6. 26
      apps/explorer/lib/explorer/chain.ex
  7. 33
      apps/explorer/lib/explorer/chain/decompiled_smart_contract.ex
  8. 10
      apps/explorer/lib/explorer/chain/hash.ex
  9. 15
      apps/explorer/priv/repo/migrations/20190319081821_create_decompiled_smart_contracts.exs
  10. 11
      apps/explorer/priv/repo/migrations/20190325081658_remove_unique_address_hash_decompiled_contracts.exs
  11. 20
      apps/explorer/test/explorer/chain/decompiled_smart_contract_test.exs
  12. 70
      apps/explorer/test/explorer/chain_test.exs
  13. 17
      apps/explorer/test/support/factory.ex

@ -3,6 +3,8 @@
### Features ### Features
- [1611](https://github.com/poanetwork/blockscout/pull/1611) - allow setting the first indexing block - [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 ### Fixes

@ -10,7 +10,8 @@ config :block_scout_web,
namespace: BlockScoutWeb, namespace: BlockScoutWeb,
ecto_repos: [Explorer.Repo], ecto_repos: [Explorer.Repo],
version: System.get_env("BLOCKSCOUT_VERSION"), version: System.get_env("BLOCKSCOUT_VERSION"),
release_link: System.get_env("RELEASE_LINK") release_link: System.get_env("RELEASE_LINK"),
decompiled_smart_contract_token: System.get_env("DECOMPILED_SMART_CONTRACT_TOKEN")
config :block_scout_web, BlockScoutWeb.Chain, config :block_scout_web, BlockScoutWeb.Chain,
network: System.get_env("NETWORK"), network: System.get_env("NETWORK"),

@ -0,0 +1,79 @@
defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.Chain.Hash.Address
def create(conn, params) do
if auth_token(conn) == actual_token() do
with {:ok, hash} <- validate_address_hash(params["address_hash"]),
:ok <- smart_contract_exists?(hash),
:ok <- decompiled_contract_exists?(params["address_hash"], params["decompiler_version"]) do
case Chain.create_decompiled_smart_contract(params) do
{:ok, decompiled_smart_contract} ->
send_resp(conn, :created, Jason.encode!(decompiled_smart_contract))
{:error, changeset} ->
errors =
changeset.errors
|> Enum.into(%{}, fn {field, {message, _}} ->
{field, message}
end)
send_resp(conn, :unprocessable_entity, encode(errors))
end
else
:invalid_address ->
send_resp(conn, :unprocessable_entity, encode(%{error: "address_hash is invalid"}))
:not_found ->
send_resp(conn, :unprocessable_entity, encode(%{error: "address is not found"}))
:contract_exists ->
send_resp(
conn,
:unprocessable_entity,
encode(%{error: "decompiled code already exists for the decompiler version"})
)
end
else
send_resp(conn, :forbidden, "")
end
end
defp smart_contract_exists?(address_hash) do
case Chain.hash_to_address(address_hash) do
{:ok, _address} -> :ok
_ -> :not_found
end
end
defp validate_address_hash(address_hash) do
case Address.cast(address_hash) do
{:ok, hash} -> {:ok, hash}
:error -> :invalid_address
end
end
defp decompiled_contract_exists?(address_hash, decompiler_version) do
case Chain.decompiled_code(address_hash, decompiler_version) do
{:ok, _} -> :contract_exists
_ -> :ok
end
end
defp auth_token(conn) do
case get_req_header(conn, "auth_token") do
[token] -> token
other -> other
end
end
defp actual_token do
Application.get_env(:block_scout_web, :decompiled_smart_contract_token)
end
defp encode(data) do
Jason.encode!(data)
end
end

@ -22,6 +22,8 @@ defmodule BlockScoutWeb.Router do
pipe_through(:api) pipe_through(:api)
get("/supply", SupplyController, :supply) get("/supply", SupplyController, :supply)
resources("/decompiled_smart_contract", DecompiledSmartContractController, only: [:create])
end end
scope "/api", BlockScoutWeb.API.RPC do scope "/api", BlockScoutWeb.API.RPC do

@ -0,0 +1,99 @@
defmodule BlockScoutWeb.API.V1.DecompiledControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Repo
alias Explorer.Chain.DecompiledSmartContract
import Ecto.Query,
only: [from: 2]
@secret "secret"
describe "when used authorized" do
setup %{conn: conn} = context do
Application.put_env(:block_scout_web, :decompiled_smart_contract_token, @secret)
auth_conn = conn |> put_req_header("auth_token", @secret)
{:ok, Map.put(context, :conn, auth_conn)}
end
test "returns unprocessable_entity status when params are invalid", %{conn: conn} do
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create))
assert request.status == 422
assert request.resp_body == "{\"error\":\"address_hash is invalid\"}"
end
test "returns unprocessable_entity when code is empty", %{conn: conn} do
decompiler_version = "test_decompiler"
address = insert(:address)
params = %{
"address_hash" => to_string(address.hash),
"decompiler_version" => decompiler_version
}
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create), params)
assert request.status == 422
assert request.resp_body == "{\"decompiled_source_code\":\"can't be blank\"}"
end
test "can not update code for the same decompiler version", %{conn: conn} do
address_hash = to_string(insert(:address, hash: "0x0000000000000000000000000000000000000001").hash)
decompiler_version = "test_decompiler"
decompiled_source_code = "hello world"
insert(:decompiled_smart_contract,
address_hash: address_hash,
decompiler_version: decompiler_version,
decompiled_source_code: decompiled_source_code
)
params = %{
"address_hash" => address_hash,
"decompiler_version" => decompiler_version,
"decompiled_source_code" => decompiled_source_code
}
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create), params)
assert request.status == 422
assert request.resp_body == "{\"error\":\"decompiled code already exists for the decompiler version\"}"
end
test "creates decompiled smart contract", %{conn: conn} do
address_hash = to_string(insert(:address, hash: "0x0000000000000000000000000000000000000001").hash)
decompiler_version = "test_decompiler"
decompiled_source_code = "hello world"
params = %{
"address_hash" => address_hash,
"decompiler_version" => decompiler_version,
"decompiled_source_code" => decompiled_source_code
}
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create), params)
assert request.status == 201
assert request.resp_body ==
"{\"address_hash\":\"0x0000000000000000000000000000000000000001\",\"decompiler_version\":\"test_decompiler\",\"decompiled_source_code\":\"hello world\"}"
decompiled_smart_contract = Repo.one!(from(d in DecompiledSmartContract, where: d.address_hash == ^address_hash))
assert to_string(decompiled_smart_contract.address_hash) == address_hash
assert decompiled_smart_contract.decompiler_version == decompiler_version
assert decompiled_smart_contract.decompiled_source_code == decompiled_source_code
end
end
describe "when user is not authorized" do
test "returns forbedden", %{conn: conn} do
request = post(conn, api_v1_decompiled_smart_contract_path(conn, :create))
assert request.status == 403
end
end
end

@ -31,6 +31,7 @@ defmodule Explorer.Chain do
Block, Block,
BlockNumberCache, BlockNumberCache,
Data, Data,
DecompiledSmartContract,
Hash, Hash,
Import, Import,
InternalTransaction, InternalTransaction,
@ -512,6 +513,17 @@ defmodule Explorer.Chain do
|> Repo.insert() |> Repo.insert()
end end
@doc """
Creates a decompiled smart contract.
"""
@spec create_decompiled_smart_contract(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def create_decompiled_smart_contract(attrs) do
%DecompiledSmartContract{}
|> DecompiledSmartContract.changeset(attrs)
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:decompiler_version, :address_hash])
end
@doc """ @doc """
Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users efficiently. Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users efficiently.
@ -656,6 +668,20 @@ defmodule Explorer.Chain do
end end
end end
def decompiled_code(address_hash, version) do
query =
from(contract in DecompiledSmartContract,
where: contract.address_hash == ^address_hash and contract.decompiler_version == ^version
)
query
|> Repo.one()
|> case do
nil -> {:error, :not_found}
contract -> {:ok, contract.decompiled_source_code}
end
end
@spec token_contract_address_from_token_name(String.t()) :: {:ok, Hash.Address.t()} | {:error, :not_found} @spec token_contract_address_from_token_name(String.t()) :: {:ok, Hash.Address.t()} | {:error, :not_found}
def token_contract_address_from_token_name(name) when is_binary(name) do def token_contract_address_from_token_name(name) when is_binary(name) do
query = query =

@ -0,0 +1,33 @@
defmodule Explorer.Chain.DecompiledSmartContract do
@moduledoc """
The representation of a decompiled smart contract.
"""
use Explorer.Schema
alias Explorer.Chain.{Address, Hash}
@derive {Jason.Encoder, only: [:address_hash, :decompiler_version, :decompiled_source_code]}
schema "decompiled_smart_contracts" do
field(:decompiler_version, :string)
field(:decompiled_source_code, :string)
belongs_to(
:address,
Address,
foreign_key: :address_hash,
references: :hash,
type: Hash.Address
)
timestamps()
end
def changeset(%__MODULE__{} = smart_contract, attrs) do
smart_contract
|> cast(attrs, [:decompiler_version, :decompiled_source_code, :address_hash])
|> validate_required([:decompiler_version, :decompiled_source_code, :address_hash])
|> unique_constraint(:address_hash)
end
end

@ -233,4 +233,14 @@ defmodule Explorer.Chain.Hash do
|> BitString.encode(options) |> BitString.encode(options)
end end
end end
defimpl Jason.Encoder do
alias Jason.Encode
def encode(hash, opts) do
hash
|> to_string()
|> Encode.string(opts)
end
end
end end

@ -0,0 +1,15 @@
defmodule Explorer.Repo.Migrations.CreateDecompiledSmartContracts do
use Ecto.Migration
def change do
create table(:decompiled_smart_contracts) do
add(:decompiler_version, :string, null: false)
add(:decompiled_source_code, :text, null: false)
add(:address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false)
timestamps()
end
create(unique_index(:decompiled_smart_contracts, :address_hash))
end
end

@ -0,0 +1,11 @@
defmodule Explorer.Repo.Migrations.RemoveUniqueAddressHashDecompiledContracts do
use Ecto.Migration
def change do
drop(index(:decompiled_smart_contracts, [:address_hash]))
create(
unique_index(:decompiled_smart_contracts, [:address_hash, :decompiler_version], name: :address_decompiler_version)
)
end
end

@ -0,0 +1,20 @@
defmodule Explorer.Chain.DecompiledSmartContractTest do
use Explorer.DataCase
alias Explorer.Chain.DecompiledSmartContract
describe "changeset/2" do
test "with valid attributes" do
params = params_for(:decompiled_smart_contract)
changeset = DecompiledSmartContract.changeset(%DecompiledSmartContract{}, params)
assert changeset.valid?
end
test "with invalid attributes" do
changeset = DecompiledSmartContract.changeset(%DecompiledSmartContract{}, %{elixir: "erlang"})
refute changeset.valid?
end
end
end

@ -14,6 +14,7 @@ defmodule Explorer.ChainTest do
Address, Address,
Block, Block,
Data, Data,
DecompiledSmartContract,
Hash, Hash,
InternalTransaction, InternalTransaction,
Log, Log,
@ -2586,6 +2587,75 @@ defmodule Explorer.ChainTest do
end end
end end
describe "create_decompiled_smart_contract/1" do
test "with valid params creates decompiled smart contract" do
address_hash = to_string(insert(:address).hash)
decompiler_version = "test_decompiler"
decompiled_source_code = "hello world"
params = %{
address_hash: address_hash,
decompiler_version: decompiler_version,
decompiled_source_code: decompiled_source_code
}
{:ok, decompiled_smart_contract} = Chain.create_decompiled_smart_contract(params)
assert decompiled_smart_contract.decompiler_version == decompiler_version
assert decompiled_smart_contract.decompiled_source_code == decompiled_source_code
assert address_hash == to_string(decompiled_smart_contract.address_hash)
end
test "with invalid params can't create decompiled smart contract" do
params = %{code: "cat"}
{:error, _changeset} = Chain.create_decompiled_smart_contract(params)
end
test "updates smart contract code" do
inserted_decompiled_smart_contract = insert(:decompiled_smart_contract)
code = "code2"
{:ok, _decompiled_smart_contract} =
Chain.create_decompiled_smart_contract(%{
decompiler_version: inserted_decompiled_smart_contract.decompiler_version,
decompiled_source_code: code,
address_hash: inserted_decompiled_smart_contract.address_hash
})
decompiled_smart_contract =
Repo.one(
from(ds in DecompiledSmartContract,
where:
ds.address_hash == ^inserted_decompiled_smart_contract.address_hash and
ds.decompiler_version == ^inserted_decompiled_smart_contract.decompiler_version
)
)
assert decompiled_smart_contract.decompiled_source_code == code
end
test "creates two smart contracts for different decompiler versions" do
inserted_decompiled_smart_contract = insert(:decompiled_smart_contract)
code = "code2"
version = "2"
{:ok, _decompiled_smart_contract} =
Chain.create_decompiled_smart_contract(%{
decompiler_version: version,
decompiled_source_code: code,
address_hash: inserted_decompiled_smart_contract.address_hash
})
decompiled_smart_contracts =
Repo.all(
from(ds in DecompiledSmartContract, where: ds.address_hash == ^inserted_decompiled_smart_contract.address_hash)
)
assert Enum.count(decompiled_smart_contracts) == 2
end
end
describe "create_smart_contract/1" do describe "create_smart_contract/1" do
setup do setup do
smart_contract_bytecode = smart_contract_bytecode =

@ -19,6 +19,7 @@ defmodule Explorer.Factory do
Block, Block,
ContractMethod, ContractMethod,
Data, Data,
DecompiledSmartContract,
Hash, Hash,
InternalTransaction, InternalTransaction,
Log, Log,
@ -509,7 +510,7 @@ defmodule Explorer.Factory do
} }
end end
def smart_contract_factory() do def smart_contract_factory do
contract_code_info = contract_code_info() contract_code_info = contract_code_info()
%SmartContract{ %SmartContract{
@ -522,7 +523,17 @@ defmodule Explorer.Factory do
} }
end end
def token_balance_factory() do def decompiled_smart_contract_factory do
contract_code_info = contract_code_info()
%DecompiledSmartContract{
address_hash: insert(:address, contract_code: contract_code_info.bytecode).hash,
decompiler_version: "test_decompiler",
decompiled_source_code: contract_code_info.source_code
}
end
def token_balance_factory do
%TokenBalance{ %TokenBalance{
address: build(:address), address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash, token_contract_address_hash: insert(:token).contract_address_hash,
@ -532,7 +543,7 @@ defmodule Explorer.Factory do
} }
end end
def address_current_token_balance_factory() do def address_current_token_balance_factory do
%CurrentTokenBalance{ %CurrentTokenBalance{
address: build(:address), address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash, token_contract_address_hash: insert(:token).contract_address_hash,

Loading…
Cancel
Save