diff --git a/apps/indexer/lib/indexer/block/util.ex b/apps/indexer/lib/indexer/block/util.ex new file mode 100644 index 0000000000..ad603ea10c --- /dev/null +++ b/apps/indexer/lib/indexer/block/util.ex @@ -0,0 +1,75 @@ +defmodule Indexer.Block.Util do + @moduledoc """ + Helper functions for parsing block information. + """ + + @doc """ + Calculates the signer's address by recovering the ECDSA public key. + + https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm + """ + def signer(block) when is_map(block) do + # Last 65 bytes is the signature. Multiply by two since we haven't transformed to raw bytes + {extra_data, signature} = String.split_at(trim_prefix(block.extra_data), -130) + + block = %{block | extra_data: extra_data} + + signature_hash = signature_hash(block) + + recover_pub_key(signature_hash, decode(signature)) + end + + # Signature hash calculated from the block header. + # Needed for PoA-based chains + defp signature_hash(block) do + header_data = [ + decode(block.parent_hash), + decode(block.sha3_uncles), + decode(block.miner), + decode(block.state_root), + decode(block.transactions_root), + decode(block.receipts_root), + decode(block.logs_bloom), + block.difficulty, + block.number, + block.gas_limit, + block.gas_used, + block.timestamp, + decode(block.extra_data), + decode(block.mix_hash), + decode(block.nonce) + ] + + :keccakf1600.hash(:sha3_256, ExRLP.encode(header_data)) + end + + defp trim_prefix("0x" <> rest), do: rest + + defp decode("0x" <> rest) do + decode(rest) + end + + defp decode(data) do + Base.decode16!(data, case: :mixed) + end + + # Recovers the key from the signature hash and signature + defp recover_pub_key(signature_hash, signature) do + << + r::bytes-size(32), + s::bytes-size(32), + v::integer-size(8) + >> = signature + + # First byte represents compression which can be ignored + # Private key is the last 64 bytes + {:ok, <<_compression::bytes-size(1), private_key::binary>>} = + :libsecp256k1.ecdsa_recover_compact(signature_hash, r <> s, :uncompressed, v) + + # Public key comes from the last 20 bytes + <<_::bytes-size(12), public_key::binary>> = :keccakf1600.hash(:sha3_256, private_key) + + miner_address = Base.encode16(public_key, case: :lower) + "0x" <> miner_address + end +end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index f8b26a7f47..44b5d3dea5 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -46,10 +46,14 @@ defmodule Indexer.MixProject do [ # JSONRPC access to Parity for `Explorer.Indexer` {:ethereum_jsonrpc, in_umbrella: true}, + # RLP encoding + {:ex_rlp, "~> 0.3"}, # Code coverage {:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"}, # Importing to database {:explorer, in_umbrella: true}, + # libsecp2561k1 crypto functions + {:libsecp256k1, "~> 0.1.10"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing diff --git a/apps/indexer/test/indexer/block/util_test.exs b/apps/indexer/test/indexer/block/util_test.exs new file mode 100644 index 0000000000..c75875d038 --- /dev/null +++ b/apps/indexer/test/indexer/block/util_test.exs @@ -0,0 +1,40 @@ +defmodule Indexer.Block.UtilTest do + use ExUnit.Case + + alias Indexer.Block.Util + + test "signer/1" do + data = %{ + difficulty: 1, + extra_data: + "0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00", + gas_limit: 7_753_377, + gas_used: 1_810_195, + hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f", + logs_bloom: + "0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000", + miner: "0x0000000000000000000000000000000000000000", + mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + nonce: "0x0000000000000000", + number: 2_848_394, + parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df", + receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + size: 6437, + state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92", + timestamp: 1_534_796_040, + total_difficulty: 5_353_647, + transactions: [ + "0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5", + "0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb", + "0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2", + "0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd", + "0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a" + ], + transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4", + uncles: [] + } + + assert Util.signer(data) == "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91" + end +end diff --git a/mix.lock b/mix.lock index 8c75e5a4c7..4a62ae5e91 100644 --- a/mix.lock +++ b/mix.lock @@ -32,6 +32,7 @@ "ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, optional: false]}]}, "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.2.1", "df84d0b23487aaa8570c35e586d7f9f197a7787e1121344a41d8832a7ea41edf", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_rlp": {:hex, :ex_rlp, "0.3.1", "190554f7b26f79734fc5a772241eec14a71b2e83576e43f451479feb017013e9", [:mix], [], "hexpm"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], []}, "excoveralls": {:git, "https://github.com/KronicDeth/excoveralls.git", "0a859b68851eeba9b43eba59fbc8f9098299cfe1", [branch: "circle-workflows"]}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, @@ -50,7 +51,7 @@ "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], []}, "junit_formatter": {:hex, :junit_formatter, "2.2.0", "da6093f0740c58a824f9585ebb7cb1b960efaecf48d1fa969e95d9c47c6b19dd", [:mix], [], "hexpm"}, "keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], []}, - "libsecp256k1": {:hex, :libsecp256k1, "0.1.4", "42b7f76d8e32f85f578ccda0abfdb1afa0c5c231d1fd8aeab9cda352731a2d83", [:rebar3], []}, + "libsecp256k1": {:hex, :libsecp256k1, "0.1.10", "d27495e2b9851c7765129b76c53b60f5e275bd6ff68292c50536bf6b8d091a4d", [:make, :mix], [{:mix_erlang_tasks, "0.1.0", [hex: :mix_erlang_tasks, repo: "hexpm", optional: false]}], "hexpm"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.10", "876f9f84ae110781207c54321ffbb62bebe02946fe3c13f0d7c5f5d8ad4fa910", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, @@ -59,6 +60,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "mix_erlang_tasks": {:hex, :mix_erlang_tasks, "0.1.0", "36819fec60b80689eb1380938675af215565a89320a9e29c72c70d97512e4649", [:mix], [], "hexpm"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"},