Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve list of nodes via DNS #63

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion eth_client/lib/eth_client/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ defmodule EthClient.Application do
}

children = [
{EthClient.Context, initial_context}
{EthClient.Context, initial_context},
{EthClient.NodesList, EthClient.NodesList.DNS.Storage}
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
53 changes: 53 additions & 0 deletions eth_client/lib/eth_client/nodes_list.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule EthClient.NodesList do
@moduledoc """
Retrieve a storage list for a specific network. Right now, only supports getting a list
by DNS.
"""
use GenServer

alias EthClient.NodesList.DNS, as: NodesListDNS
alias EthClient.NodesList.Storage

@type network :: :mainnet | :ropsten | :rinkeby | :goerli

def start_link(storage_name) do
GenServer.start_link(__MODULE__, storage_name, name: __MODULE__)
end

@spec update_using_dns(network()) :: :ok | {:error, term()}
def update_using_dns(network) do
GenServer.call(__MODULE__, {:update_using_dns, network}, search_timeout())
end

@spec get(network()) :: :ok | {:error, term()}
def get(network) do
GenServer.call(__MODULE__, {:get, network})
end

def search_timeout, do: 20_000

## Server callbacks

@impl true
def init(storage_name) do
storage = Storage.new(storage_name)
{:ok, storage}
end

@impl true
def handle_call({:update_using_dns, network}, _from, storage) do
result =
with {:ok, enr_root} <- NodesListDNS.get_root(network) do
NodesListDNS.start_searching_for_nodes(network, storage, enr_root)
end

{:reply, result, storage}
end

@impl true
def handle_call({:get, network}, _from, storage) do
result = NodesListDNS.get_nodes(network, storage)

{:reply, result, storage}
end
end
97 changes: 97 additions & 0 deletions eth_client/lib/eth_client/nodes_list/dns.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
defmodule EthClient.NodesList.DNS do
@moduledoc """
Module for handling storage list retrieval using DNS.
"""
alias EthClient.NodesList
alias EthClient.NodesList.Storage

@enr_prefix "enr:"
@enrtree_branch_prefix "enrtree-branch:"
@attempts 100
@waiting_time_ms 500

def start_searching_for_nodes(network, storage, enr_root) do
Storage.delete(storage, network)
search = Task.async(fn -> get_children(network, storage, enr_root) end)
Task.await(search, NodesList.search_timeout())
end

def get_root(network) do
{:ok, response_split} =
network
|> get_domain_name()
|> DNS.resolve(:txt)

response = Enum.join(response_split)

case Regex.run(~r/^enrtree-root:v1 e=([\w\d]+) .*$/, response) do
[^response, enr_root] -> {:ok, enr_root}
nil -> {:error, "enr_root not found in DNS response: #{response}"}
end
end

def get_nodes(network, storage), do: Storage.lookup(storage, network)

defp get_children(network, storage, branch) do
branch_domain_name = branch <> "." <> get_domain_name(network)
{:ok, response_split} = wait_to_resolve(branch_domain_name)
response = Enum.join(response_split)
parse_child(network, storage, response)
end

defp parse_child(network, storage, @enr_prefix <> new_node) do
decoded_node =
new_node
|> Base.url_decode64!(padding: false)
|> ExRLP.decode()

Storage.insert(storage, network, decoded_node)
:ok
end

defp parse_child(network, storage, @enrtree_branch_prefix <> branches) do
branches
|> String.split(",")
|> Enum.map(fn branch ->
Task.async(fn -> get_children(network, storage, branch) end)
end)
|> Task.await_many(NodesList.search_timeout())

:ok
end

defp parse_child(_, _, response) do
{:error,
"Neither #{@enr_prefix} nor #{@enrtree_branch_prefix} is in DNS response: #{response}"}
end

defp wait_to_resolve(domain_name), do: wait_to_resolve(domain_name, @attempts)

defp wait_to_resolve(domain_name, 0), do: {:error, "Cannot resolve #{domain_name}"}

defp wait_to_resolve(domain_name, attempts) do
response =
try do
try_to_resolve(domain_name, :start)
rescue
Socket.Error -> {:error, attempts}
end

try_to_resolve(domain_name, response)
end

defp try_to_resolve(domain_name, :start), do: DNS.resolve(domain_name, :txt)

defp try_to_resolve(domain_name, {:error, attempts}) do
Process.sleep(@waiting_time_ms)
wait_to_resolve(domain_name, attempts - 1)
end

defp try_to_resolve(_domain_name, {:ok, _} = success), do: success

@spec get_domain_name(NodesList.network()) :: String.t()
defp get_domain_name(:mainnet), do: "all.mainnet.ethdisco.net"
defp get_domain_name(:ropsten), do: "all.ropsten.ethdisco.net"
defp get_domain_name(:rinkeby), do: "all.rinkeby.ethdisco.net"
defp get_domain_name(:goerli), do: "all.goerli.ethdisco.net"
end
24 changes: 24 additions & 0 deletions eth_client/lib/eth_client/nodes_list/storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule EthClient.NodesList.Storage do
@moduledoc """
Module for handling nodes list storage.
"""

def new(name) do
:ets.new(name, [:named_table, :public, read_concurrency: true])
end

def lookup(storage, network) do
with [{^network, nodes}] <- :ets.lookup(storage, network) do
nodes
end
end

def insert(storage, network, new_node) do
network_nodes = lookup(storage, network)
:ets.insert(storage, {network, [new_node | network_nodes]})
end

def delete(storage, network) do
:ets.delete(storage, network)
end
end
3 changes: 2 additions & 1 deletion eth_client/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ defmodule EthClient.MixProject do
{:rustler, "~> 0.25.0"},
{:ex_rlp, "~> 0.5.4"},
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.14.5", only: [:test], runtime: false}
{:excoveralls, "~> 0.14.5", only: [:test], runtime: false},
{:dns, "~> 2.3.0"}
]
end
end
2 changes: 2 additions & 0 deletions eth_client/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"dns": {:hex, :dns, "2.3.0", "3e750cf3e229898628a091fa7dca29524294db2962d0fc15696ac3a5cfff2315", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm", "f3f1074409b4ecf6f93379a49fb0aef30a898f3093ea22b5c64a85e9f99ca6f8"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_abi": {:hex, :ex_abi, "0.5.11", "a53307cf796231bf068a9941d57fbcb8654e72042c2a113a49f08dfb248875fb", [:mix], [{:ex_keccak, "~> 0.4.0", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e128577740bdc0f05ed6841cbb1c88bb80377613970ed870fa052b780870655a"},
"ex_keccak": {:hex, :ex_keccak, "0.4.0", "4eb8620c8a20a546e2d297b5ce3de150a90db0fdc4ba1dd88c854ace9ee47603", [:mix], [{:rustler, "~> 0.24", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "209ec5591d3cf79f9bdcdedf39c3d7a1fb208321e2b6de2660801117ae386f10"},
Expand All @@ -17,6 +18,7 @@
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"rustler": {:hex, :rustler, "0.25.0", "32526b51af7e58a740f61941bf923486ce6415a91c3934cc16c281aa201a2240", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "6b43a11a37fe79c6234d88c4102ab5dfede7a6a764dc5c7b539956cfa02f3cf4"},
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
"toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"},
Expand Down