├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── chains └── ropsten.json ├── config ├── config.exs ├── dev.exs └── test.exs ├── lib ├── ex_wire.ex └── ex_wire │ ├── adapter │ ├── tcp.ex │ └── udp.ex │ ├── config.ex │ ├── crypto.ex │ ├── discovery.ex │ ├── exth.ex │ ├── framing │ ├── frame.ex │ └── secrets.ex │ ├── handler.ex │ ├── handler │ ├── find_neighbours.ex │ ├── neighbours.ex │ ├── ping.ex │ └── pong.ex │ ├── handshake │ ├── eip_8.ex │ ├── handshake.ex │ └── struct │ │ ├── ack_resp_v4.ex │ │ └── auth_msg_v4.ex │ ├── message.ex │ ├── message │ ├── find_neighbours.ex │ ├── neighbours.ex │ ├── ping.ex │ └── pong.ex │ ├── network.ex │ ├── packet.ex │ ├── packet │ ├── block_bodies.ex │ ├── block_headers.ex │ ├── disconnect.ex │ ├── get_block_bodies.ex │ ├── get_block_headers.ex │ ├── hello.ex │ ├── new_block.ex │ ├── new_block_hashes.ex │ ├── ping.ex │ ├── pong.ex │ ├── status.ex │ └── transactions.ex │ ├── peer_supervisor.ex │ ├── protocol.ex │ ├── struct │ ├── block.ex │ ├── block_queue.ex │ ├── endpoint.ex │ ├── neighbour.ex │ └── peer.ex │ ├── sync.ex │ └── util │ └── timestamp.ex ├── mix.exs ├── mix.lock └── test ├── ex_wire ├── adapters │ └── udp_test.exs ├── config_test.exs ├── crypto_test.exs ├── framing │ ├── frame_test.exs │ └── secrets_test.exs ├── handler │ ├── find_neighbours_test.exs │ └── ping_test.exs ├── handler_test.exs ├── handshake │ ├── eip_8_test.exs │ ├── handshake_test.exs │ └── struct │ │ ├── ack_resp_v4_test.exs │ │ └── auth_msg_v4_test.exs ├── message │ ├── find_neighbours_test.exs │ ├── neighbours_test.exs │ ├── ping_test.exs │ └── pong_test.exs ├── message_test.exs ├── network_test.exs ├── packet │ ├── block_bodies_test.exs │ ├── block_headers_test.exs │ ├── disconnect_test.exs │ ├── get_block_bodies_test.exs │ ├── get_block_headers_test.exs │ ├── hello_test.exs │ ├── new_block_hashes_test.exs │ ├── new_block_test.exs │ ├── ping_test.exs │ ├── pong_test.exs │ ├── status_test.exs │ └── transactions_test.exs ├── packet_test.exs ├── peer_supervisor_test.exs ├── protocol_test.exs ├── struct │ ├── block_queue_test.exs │ ├── block_test.exs │ ├── endpoint_test.exs │ ├── neighbour_test.exs │ └── peer_test.exs ├── sync_test.exs └── util │ └── timestamp_test.exs ├── ex_wire_test.exs ├── integration ├── remote_connection_test.exs └── wire_to_wire_test.exs ├── support └── ex_wire │ └── adapter │ └── test.ex └── test_helper.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | working_directory: ~/ex_wire 5 | docker: 6 | - image: elixir:latest 7 | steps: 8 | - run: apt-get update; apt-get -y install libgmp3-dev 9 | - checkout 10 | - restore_cache: 11 | key: _build 12 | - run: mix local.hex --force 13 | - run: mix local.rebar --force 14 | - run: mix deps.get 15 | - run: mix test --exclude network 16 | - run: mix dialyzer 17 | - save_cache: 18 | key: _build 19 | paths: 20 | - _build 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | *.DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.2 2 | * Build proper Discovery and Peering model 3 | * Add NAT traversal via UPnP 4 | * Default bind to random port 5 | # 0.1.1 6 | * Add RLPx, DevP2P and Eth Wire Protocol Support 7 | * Add syncing support from remote peers 8 | # 0.1.0 9 | * Add Discovery Protocol Support -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2017 Geoffrey Hayes, Ayrat Badykov, Mason Fischer 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExWire [![CircleCI](https://circleci.com/gh/exthereum/ex_wire.svg?style=svg)](https://circleci.com/gh/exthereum/ex_wire) 2 | 3 | Elixir Client for RLPx, DevP2P and Eth Wire Protocol. 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `ex_wire` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [{:ex_wire, "~> 0.1.2"}] 13 | end 14 | ``` 15 | 16 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 17 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 18 | be found at [https://hexdocs.pm/ex_wire](https://hexdocs.pm/ex_wire). 19 | 20 | ## Usage 21 | 22 | For the time being, this can be used to sync the block chain from a given network (currently defaulted to Ropsten). 23 | 24 | You can run `iex -S mix` and you should see: 25 | 26 | ``` 27 | 12:59:23.818 [debug] [Network] [6ce059...1acd9d] Established outbound connection with 13.84.180.240, sending auth. 28 | 29 | 12:59:23.858 [debug] [Network] Sending EIP8 Handshake to 13.84.180.240 30 | 31 | 12:59:23.884 [debug] [Network] [6ce059...1acd9d] Sending raw data message of length 388 byte(s) to 13.84.180.240 32 | 33 | 12:59:23.886 [debug] [Sync] Requesting block 0 34 | 35 | ... 36 | 37 | 12:59:24.496 [debug] [Packet] Peer sent 1 header(s) 38 | 39 | 12:59:24.540 [debug] [Block Queue] Verified block and added to new block tree 40 | 41 | 12:59:24.540 [debug] [Sync] Requesting block 1 42 | 43 | 12:59:24.541 [info] [Network] [6ce059...1acd9d] Sending packet Elixir.ExWire.Packet.GetBlockHeaders to 13.84.180.240 44 | 45 | 12:59:24.593 [debug] [Network] [6ce059...1acd9d] Got packet Elixir.ExWire.Packet.BlockHeaders from 13.84.180.240 46 | 47 | 12:59:24.593 [debug] [Packet] Peer sent 1 header(s) 48 | 49 | 12:59:24.595 [debug] [Block Queue] Verified block and added to new block tree 50 | ``` 51 | 52 | In the future, we will continue to grow and built out a proper syncing ability. It's likely the proper interface (with CLI tools) will not be this module directly, but instead a general CLI which calls into this module. 53 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :ex_wire, 6 | p2p_version: 0x04, 7 | protocol_version: 63, 8 | network_id: 3, # ropsten 9 | caps: [{"eth", 62}, {"eth", 63}], 10 | chain: :ropsten, 11 | private_key: :random, # TODO: This should be set and stored in a file 12 | bootnodes: :from_chain, 13 | commitment_count: 1 # Number of peer advertisements before we trust a block 14 | 15 | import_config "#{Mix.env}.exs" 16 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ex_wire, 4 | network_adapter: ExWire.Adapter.UDP, 5 | sync: true, 6 | use_nat: true, 7 | local_ip: {127, 0, 0, 1}, 8 | commitment_count: 2 # Number of peer advertisements before we trust a block 9 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ex_wire, 4 | network_adapter: ExWire.Adapter.Test, 5 | private_key: :random 6 | -------------------------------------------------------------------------------- /lib/ex_wire.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire do 2 | @moduledoc """ 3 | Main application for ExWire. We will begin listening on a port 4 | when this application is started. 5 | """ 6 | 7 | @default_network_adapter Application.get_env(:ex_wire, :network_adapter) 8 | 9 | @type node_id :: binary() 10 | 11 | use Application 12 | 13 | def start(_type, args) do 14 | import Supervisor.Spec 15 | 16 | network_adapter = Keyword.get(args, :network_adapter, @default_network_adapter) 17 | port = Keyword.get(args, :port, ExWire.Config.listen_port()) 18 | name = Keyword.get(args, :name, ExWire) 19 | 20 | sync_children = if ExWire.Config.sync do 21 | # TODO: Replace with level db 22 | db = MerklePatriciaTree.Test.random_ets_db() 23 | 24 | [ 25 | worker(ExWire.Discovery, [ExWire.Config.bootnodes]), 26 | worker(ExWire.PeerSupervisor, [:ok]), 27 | worker(ExWire.Sync, [db]) 28 | ] 29 | else 30 | [] 31 | end 32 | 33 | discovery = if ExWire.Config.sync, do: ExWire.Discovery, else: nil 34 | 35 | children = [ 36 | worker(network_adapter, [{ExWire.Network, [discovery]}, port], name: ExWire.Network, restart: :permanent), 37 | ] ++ sync_children 38 | 39 | opts = [strategy: :one_for_one, name: name] 40 | Supervisor.start_link(children, opts) 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /lib/ex_wire/adapter/tcp.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Adapter.TCP do 2 | @moduledoc """ 3 | Starts a TCP server to handle incoming and outgoing RLPx, DevP2P, Eth Wire connection. 4 | 5 | Once this connection is up, it's possible to add a subscriber to the different packets 6 | that are sent over the connection. This is the primary way of handling packets. 7 | 8 | Note: incoming connections are not fully tested at this moment. 9 | Note: we do not currently store token to restart connections (this upsets some peers) 10 | """ 11 | use GenServer 12 | 13 | require Logger 14 | 15 | alias ExWire.Framing.Frame 16 | alias ExWire.Handshake 17 | alias ExWire.Packet 18 | 19 | @ping_interval 2_000 20 | 21 | @doc """ 22 | Starts an outbound peer to peer connection. 23 | """ 24 | def start_link(:outbound, peer, subscribers \\ []) do 25 | GenServer.start_link(__MODULE__, %{is_outbound: true, peer: peer, active: false, closed: false, subscribers: subscribers}) 26 | end 27 | 28 | @doc """ 29 | Initialize by opening up a `gen_tcp` connection to given host and port. 30 | 31 | We'll also prepare and send out an authentication message immediately after connecting. 32 | """ 33 | def init(state=%{is_outbound: true, peer: peer}) do 34 | timer_ref = :erlang.start_timer(@ping_interval, self(), :pinger) 35 | 36 | {:ok, socket} = :gen_tcp.connect(peer.host |> String.to_charlist, peer.port, [:binary]) 37 | 38 | Logger.debug("[Network] [#{peer}] Established outbound connection with #{peer.host}, sending auth.") 39 | 40 | {my_auth_msg, my_ephemeral_key_pair, my_nonce} = ExWire.Handshake.build_auth_msg( 41 | ExWire.Config.public_key(), 42 | ExWire.Config.private_key(), 43 | peer.remote_id 44 | ) 45 | 46 | {:ok, encoded_auth_msg} = my_auth_msg 47 | |> ExWire.Handshake.Struct.AuthMsgV4.serialize() 48 | |> ExWire.Handshake.EIP8.wrap_eip_8(peer.remote_id, peer.host, my_ephemeral_key_pair) 49 | 50 | # Send auth message 51 | GenServer.cast(self(), {:send, %{data: encoded_auth_msg}}) 52 | 53 | {:ok, Map.merge(state, %{ 54 | socket: socket, 55 | auth_data: encoded_auth_msg, 56 | my_ephemeral_key_pair: my_ephemeral_key_pair, 57 | my_nonce: my_nonce, 58 | timer_ref: timer_ref})} 59 | end 60 | 61 | @doc """ 62 | Allows a client to subscribe to incoming packets. Subscribers must be in the form 63 | of `{module, function, args}`, in which case we'll call `module.function(packet, ...args)`, 64 | or `{:server, server_pid}` for a GenServer, in which case we'll send a message 65 | `{:packet, packet, peer}`. 66 | """ 67 | def handle_call({:subscribe, {_module, _function, _args}=mfa}, _from, state) do 68 | updated_state = Map.update(state, :subscribers, [mfa], fn subscribers -> [mfa | subscribers] end) 69 | 70 | {:reply, :ok, updated_state} 71 | end 72 | 73 | def handle_call({:subscribe, {:server, server}=server}, _from, state) do 74 | updated_state = Map.update(state, :subscribers, [server], fn subscribers -> [server | subscribers] end) 75 | 76 | {:reply, :ok, updated_state} 77 | end 78 | 79 | @doc """ 80 | Handle info will handle when we have inbound communucation from a peer node. 81 | 82 | If we haven't yet completed our handshake, we'll await an auth or ack message 83 | as appropriate. That is, if we've established the connection and have sent an 84 | auth message, then we'll look for an ack. If we listened for a connection, we'll 85 | await an auth message. 86 | 87 | TODO: clients may send an auth before (or as) we do, and we should handle this case without error. 88 | """ 89 | def handle_info(_info={:tcp, socket, data}, state=%{is_outbound: true, peer: peer, auth_data: auth_data, my_ephemeral_key_pair: {_my_ephemeral_public_key, my_ephemeral_private_key}=_my_ephemeral_key_pair, my_nonce: my_nonce}) do 90 | case Handshake.try_handle_ack(data, auth_data, my_ephemeral_private_key, my_nonce, peer.host) do 91 | {:ok, secrets, frame_rest} -> 92 | 93 | Logger.debug("[Network] [#{peer}] Got ack from #{peer.host}, deriving secrets and sending HELLO") 94 | 95 | send_hello(self()) 96 | 97 | updated_state = Map.merge(state, %{ 98 | secrets: secrets, 99 | auth_data: nil, 100 | my_ephemeral_key_pair: nil, 101 | my_nonce: nil 102 | }) 103 | 104 | if byte_size(frame_rest) == 0 do 105 | {:noreply, updated_state} 106 | else 107 | handle_info({:tcp, socket, frame_rest}, updated_state) 108 | end 109 | {:invalid, reason} -> 110 | Logger.warn("[Network] [#{peer}] Failed to get handshake message when expecting ack - #{reason}") 111 | 112 | {:noreply, state} 113 | end 114 | end 115 | 116 | # TODO: How do we set remote id? 117 | def handle_info({:tcp, _socket, data}, state=%{is_outbound: false, peer: peer, my_ephemeral_key_pair: my_ephemeral_key_pair, my_nonce: my_nonce}) do 118 | case Handshake.try_handle_auth(data, my_ephemeral_key_pair, my_nonce, peer.remote_id, peer.host) do 119 | {:ok, ack_data, secrets} -> 120 | 121 | Logger.debug("[Network] [#{peer}] Received auth from #{peer.host}") 122 | 123 | # Send ack back to sender 124 | GenServer.cast(self(), {:send, %{data: ack_data}}) 125 | 126 | send_hello(self()) 127 | 128 | # But we're set on our secrets 129 | {:noreply, Map.merge(state, %{ 130 | secrets: secrets, 131 | auth_data: nil, 132 | my_ephemeral_key_pair: nil, 133 | my_nonce: nil 134 | })} 135 | {:invalid, reason} -> 136 | Logger.warn("[Network] [#{peer}] Received unknown handshake message when expecting auth - #{reason}") 137 | {:noreply, state} 138 | end 139 | end 140 | 141 | def handle_info(_info={:tcp, socket, data}, state=%{peer: peer, secrets: secrets}) do 142 | total_data = Map.get(state, :queued_data, <<>>) <> data 143 | 144 | case Frame.unframe(total_data, secrets) do 145 | {:ok, packet_type, packet_data, frame_rest, updated_secrets} -> 146 | # TODO: Ignore non-HELLO messages unless state is active. 147 | 148 | # TODO: Maybe move into smaller functions for testing 149 | {packet, handle_result} = case Packet.get_packet_mod(packet_type) do 150 | {:ok, packet_mod} -> 151 | Logger.debug("[Network] [#{peer}] Got packet #{Atom.to_string(packet_mod)} from #{peer.host}") 152 | 153 | packet = packet_data 154 | |> packet_mod.deserialize() 155 | 156 | {packet, packet_mod.handle(packet)} 157 | :unknown_packet_type -> 158 | Logger.warn("[Network] [#{peer}] Received unknown or unhandled packet type `#{packet_type}` from #{peer.host}") 159 | 160 | {nil, :ok} 161 | end 162 | 163 | # Updates our given state and does any actions necessary 164 | handled_state = case handle_result do 165 | :ok -> state 166 | :activate -> Map.merge(state, %{active: true}) 167 | :peer_disconnect -> 168 | # Doesn't matter if this succeeds or not 169 | :gen_tcp.shutdown(socket, :read_write) 170 | 171 | Map.merge(state, %{active: false}) 172 | {:disconnect, reason} -> 173 | Logger.warn("[Network] [#{peer}] Disconnecting to peer due to: #{Packet.Disconnect.get_reason_msg(reason)}") 174 | # TODO: Add a timeout and disconnect ourselves 175 | send_packet(self(), Packet.Disconnect.new(reason)) 176 | 177 | state 178 | {:send, packet} -> 179 | send_packet(self(), packet) 180 | 181 | state 182 | end 183 | 184 | # Let's inform any subscribers 185 | if not is_nil(packet) do 186 | for subscriber <- Map.get(state, :subscribers, []) do 187 | case subscriber do 188 | {module, function, args} -> apply(module, function, [packet | args]) 189 | {:server, server} -> send(server, {:packet, packet, peer}) 190 | end 191 | end 192 | end 193 | 194 | updated_state = Map.merge(handled_state, %{secrets: updated_secrets, queued_data: <<>>}) 195 | 196 | # If we have more packet data, we need to continue processing. 197 | if byte_size(frame_rest) == 0 do 198 | {:noreply, updated_state} 199 | else 200 | handle_info({:tcp, socket, frame_rest}, updated_state) 201 | end 202 | {:error, "Insufficent data"} -> 203 | {:noreply, Map.put(state, :queued_data, total_data)} 204 | {:error, reason} -> 205 | Logger.error("[Network] [#{peer}] Failed to read incoming packet from #{peer.host} `#{reason}`)") 206 | 207 | {:noreply, state} 208 | end 209 | end 210 | 211 | def handle_info({:tcp_closed, _socket}, state=%{peer: peer}) do 212 | Logger.warn("[Network] [#{peer}] Peer closed connection.") 213 | 214 | {:noreply, state 215 | |> Map.put(:active, false) 216 | |> Map.put(:closed, true)} 217 | end 218 | 219 | def handle_info({:timeout, _ref, :pinger}, state=%{socket: _socket, peer: peer, active: true}) do 220 | Logger.warn("[Network] [#{peer}] Sending peer ping.") 221 | 222 | send_status(self()) 223 | 224 | timer_ref = :erlang.start_timer(@ping_interval, self(), :pinger) 225 | 226 | {:noreply, Map.put(state, :timer_ref, timer_ref)} 227 | end 228 | 229 | def handle_info({:timeout, _ref, :pinger}, state=%{socket: _socket, peer: peer, active: false}) do 230 | Logger.warn("[Network] [#{peer}] Not sending peer ping.") 231 | 232 | {:noreply, state} 233 | end 234 | 235 | @doc """ 236 | If we receive a `send` before secrets are set, we'll send the data directly over the wire. 237 | """ 238 | def handle_cast({:send, %{data: data}}, state = %{socket: socket, peer: peer}) do 239 | Logger.debug("[Network] [#{peer}] Sending raw data message of length #{byte_size(data)} byte(s) to #{peer.host}") 240 | 241 | :ok = :gen_tcp.send(socket, data) 242 | 243 | {:noreply, state} 244 | end 245 | 246 | @doc """ 247 | If we receive a `send` and we have secrets set, we'll send the message as a framed Eth packet. 248 | 249 | However, if we haven't yet sent a Hello message, we should queue the message and try again later. Most 250 | servers will disconnect if we send a non-Hello message as our first message. 251 | """ 252 | def handle_cast({:send, %{packet: {packet_mod, _packet_type, _packet_data}}}=_data, state = %{peer: peer, closed: true}) do 253 | Logger.info("[Network] [#{peer}] Dropping packet #{Atom.to_string(packet_mod)} for #{peer.host} due to closed connection.") 254 | 255 | {:noreply, state} 256 | end 257 | 258 | def handle_cast({:send, %{packet: {packet_mod, _packet_type, _packet_data}}}=data, state = %{peer: peer, active: false}) when packet_mod != ExWire.Packet.Hello do 259 | Logger.info("[Network] [#{peer}] Queueing packet #{Atom.to_string(packet_mod)} to #{peer.host}") 260 | 261 | # TODO: Should we monitor this process, etc? 262 | pid = self() 263 | spawn fn -> 264 | :timer.sleep(500) 265 | 266 | GenServer.cast(pid, data) 267 | end 268 | 269 | {:noreply, state} 270 | end 271 | 272 | def handle_cast({:send, %{packet: {packet_mod, packet_type, packet_data}}}, state = %{socket: socket, secrets: secrets, peer: peer}) do 273 | Logger.info("[Network] [#{peer}] Sending packet #{Atom.to_string(packet_mod)} to #{peer.host}") 274 | 275 | {frame, updated_secrets} = Frame.frame(packet_type, packet_data, secrets) 276 | 277 | :ok = :gen_tcp.send(socket, frame) 278 | 279 | {:noreply, Map.merge(state, %{secrets: updated_secrets})} 280 | end 281 | 282 | @doc """ 283 | Client function for sending a packet over to a peer. 284 | """ 285 | @spec send_packet(pid(), struct()) :: :ok 286 | def send_packet(pid, packet) do 287 | {:ok, packet_type} = Packet.get_packet_type(packet) 288 | {:ok, packet_mod} = Packet.get_packet_mod(packet_type) 289 | packet_data = packet_mod.serialize(packet) 290 | 291 | GenServer.cast(pid, {:send, %{packet: {packet_mod, packet_type, packet_data}}}) 292 | 293 | :ok 294 | end 295 | 296 | @doc """ 297 | Client function to send HELLO message after connecting. 298 | """ 299 | def send_hello(pid) do 300 | send_packet(pid, %Packet.Hello{ 301 | p2p_version: ExWire.Config.p2p_version(), 302 | client_id: ExWire.Config.client_id(), 303 | caps: ExWire.Config.caps(), 304 | listen_port: ExWire.Config.listen_port(), 305 | node_id: ExWire.Config.node_id() 306 | }) 307 | end 308 | 309 | @doc """ 310 | Client function to send PING message. 311 | """ 312 | def send_ping(pid) do 313 | send_packet(pid, %Packet.Ping{}) 314 | end 315 | 316 | @doc """ 317 | Client function to send STATUS message. 318 | """ 319 | def send_status(pid) do 320 | send_packet(pid, %Packet.Status{ 321 | protocol_version: ExWire.Config.protocol_version(), 322 | network_id: ExWire.Config.network_id(), 323 | total_difficulty: ExWire.Config.chain.genesis.difficulty, 324 | best_hash: ExWire.Config.chain.genesis.parent_hash, 325 | genesis_hash: <<>> 326 | }) |> Exth.inspect("status") 327 | end 328 | 329 | @doc """ 330 | Client function to subscribe to incoming packets. 331 | 332 | A subscription should be in the form of `{:server, server_pid}`, and we will 333 | send a packet to that server with contents `{:packet, packet, peer}` for 334 | each received packet. 335 | """ 336 | @spec subscribe(pid(), {module(), atom(), list()} | {:server, pid()}) :: :ok 337 | def subscribe(pid, subscription) do 338 | :ok = GenServer.call(pid, {:subscribe, subscription}) 339 | end 340 | 341 | end -------------------------------------------------------------------------------- /lib/ex_wire/adapter/udp.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Adapter.UDP do 2 | @moduledoc """ 3 | Starts a UDP server to handle incoming and outgoing 4 | peer to peer messages according to RLPx. 5 | """ 6 | use GenServer 7 | use Bitwise 8 | 9 | require Logger 10 | 11 | @doc """ 12 | When starting a UDP server, we'll store a network to use for all 13 | message handling. 14 | """ 15 | def start_link({network, network_args}, port, name \\ __MODULE__) do 16 | GenServer.start_link(__MODULE__, %{network: network, network_args: network_args, port: port}, name: name) 17 | end 18 | 19 | @doc """ 20 | Initialize by opening up a `gen_udp` server on a given port. 21 | """ 22 | def init(state=%{port: port}) do 23 | {:ok, socket} = :gen_udp.open(port, [{:ip, {0, 0, 0, 0}}, {:active, true}, {:reuseaddr, true}, :binary]) 24 | {:ok, port_num} = :inet.port(socket) 25 | Logger.debug("[UDP] Listening on port #{port_num}") 26 | 27 | {:ok, Map.put(state, :socket, socket)} 28 | end 29 | 30 | @doc """ 31 | Handle info will handle when we have communucation from a peer node. 32 | 33 | We'll offload the effort to our `ExWire.Network` and `ExWire.Handler` modules. 34 | 35 | Note: all responses will be asynchronous. 36 | """ 37 | def handle_info({:udp, _socket, ip, port, data}, state=%{network: network, network_args: network_args}) do 38 | inbound_message = %ExWire.Network.InboundMessage{ 39 | data: data, 40 | server_pid: self(), 41 | remote_host: %ExWire.Struct.Endpoint{ 42 | ip: ip |> Tuple.to_list, 43 | udp_port: port, 44 | }, 45 | timestamp: ExWire.Util.Timestamp.soon(), 46 | } 47 | 48 | apply(network, :receive, [inbound_message|network_args]) 49 | 50 | {:noreply, state} 51 | end 52 | 53 | @doc """ 54 | For cast, we'll respond back to a given peer with a given message package. This represents 55 | all outbound messages we'll ever send. 56 | """ 57 | def handle_cast({:send, %{to: %{ip: ip, udp_port: udp_port}, data: data}}, state = %{socket: socket}) when not is_nil(udp_port) do 58 | # TODO: How should we handle invalid ping or message requests? 59 | :gen_udp.send(socket, ip, udp_port, data) 60 | 61 | {:noreply, state} 62 | end 63 | end -------------------------------------------------------------------------------- /lib/ex_wire/config.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Config do 2 | @moduledoc """ 3 | General configuration information for ExWire. 4 | """ 5 | 6 | @port Application.get_env(:ex_wire, :port, 30303 + :rand.uniform(10_000)) 7 | @private_key ( case Application.get_env(:ex_wire, :private_key) do 8 | key when is_binary(key) -> key 9 | :random -> ExthCrypto.ECIES.ECDH.new_ecdh_keypair() |> Tuple.to_list() |> List.last 10 | end ) 11 | @public_key ( case ExthCrypto.Signature.get_public_key(@private_key) do 12 | {:ok, public_key} -> public_key 13 | end ) 14 | @node_id @public_key |> ExthCrypto.Key.der_to_raw 15 | @protocol_version Application.get_env(:ex_wire, :protocol_version) 16 | @network_id Application.get_env(:ex_wire, :network_id) 17 | @p2p_version Application.get_env(:ex_wire, :p2p_version) 18 | @caps Application.get_env(:ex_wire, :caps) 19 | @version Mix.Project.config[:version] 20 | @sync Application.get_env(:ex_wire, :sync) 21 | @chain Application.get_env(:ex_wire, :chain) |> Blockchain.Chain.load_chain 22 | @bootnodes ( case Application.get_env(:ex_wire, :bootnodes) do 23 | nodes when is_list(nodes) -> nodes 24 | :from_chain -> @chain.nodes 25 | end ) 26 | @commitment_count Application.get_env(:ex_wire, :commitment_count) 27 | @local_ip ( case Application.get_env(:ex_wire, :local_ip, {127, 0, 0, 1}) do 28 | ip_address when is_binary(ip_address) -> 29 | {:ok, ip_address_parsed} = ip_address |> String.to_charlist |> :inet.parse_address 30 | ip_address_parsed 31 | ip_address when is_tuple(ip_address) -> ip_address 32 | end ) 33 | 34 | @use_nat Application.get_env(:ex_wire, :use_nat, false) 35 | 36 | @doc """ 37 | Returns a private key that is generated when a new session is created. It is 38 | intended that this key is semi-persisted. 39 | """ 40 | @spec private_key() :: ExthCrypto.Key.private_key() 41 | def private_key, do: @private_key 42 | 43 | @spec public_key() :: ExthCrypto.Key.public_key() 44 | def public_key, do: @public_key 45 | 46 | @spec node_id() :: ExWire.node_id 47 | def node_id, do: @node_id 48 | 49 | @spec listen_port() :: integer() 50 | def listen_port, do: @port 51 | 52 | @spec protocol_version() :: integer() 53 | def protocol_version, do: @protocol_version 54 | 55 | @spec network_id() :: integer() 56 | def network_id, do: @network_id 57 | 58 | @spec p2p_version() :: integer() 59 | def p2p_version, do: @p2p_version 60 | 61 | @spec caps() :: [{String.t, integer()}] 62 | def caps, do: @caps 63 | 64 | @spec client_id() :: String.t 65 | def client_id, do: "Exthereum/#{@version}" 66 | 67 | @spec sync() :: boolean() 68 | def sync, do: @sync 69 | 70 | @spec bootnodes() :: [String.t] 71 | def bootnodes, do: @bootnodes 72 | 73 | @spec chain() :: Blockchain.Chain.t 74 | def chain, do: @chain 75 | 76 | @spec commitment_count() :: integer() 77 | def commitment_count, do: @commitment_count 78 | 79 | @spec local_ip() :: [integer()] 80 | def local_ip, do: @local_ip 81 | 82 | @spec use_nat() :: boolean() 83 | def use_nat, do: @use_nat 84 | 85 | end -------------------------------------------------------------------------------- /lib/ex_wire/crypto.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Crypto do 2 | @moduledoc """ 3 | Helper functions for cryptographic functions of RLPx. 4 | """ 5 | 6 | @type hash :: binary() 7 | @type signature :: binary() 8 | @type recovery_id :: integer() 9 | 10 | defmodule HashMismatch do 11 | defexception [:message] 12 | end 13 | 14 | @doc """ 15 | Returns a node_id based on a given private key. 16 | 17 | ## Examples 18 | 19 | iex> ExWire.Crypto.node_id(<<1::256>>) 20 | {:ok, <<121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 21 | 206, 135, 11, 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 22 | 242, 129, 91, 22, 248, 23, 152, 72, 58, 218, 119, 38, 163, 23 | 196, 101, 93, 164, 251, 252, 14, 17, 8, 168, 253, 23, 180, 24 | 72, 166, 133, 84, 25, 156, 71, 208, 143, 251, 16, 212, 184>>} 25 | 26 | iex> ExWire.Crypto.node_id(<<1>>) 27 | {:error, "Private key size not 32 bytes"} 28 | """ 29 | @spec node_id(ExthCrypto.Key.private_key) :: {:ok, ExWire.node_id} | {:error, String.t} 30 | def node_id(private_key) do 31 | case ExthCrypto.Signature.get_public_key(private_key) do 32 | {:ok, <>} -> {:ok, public_key |> ExthCrypto.Key.der_to_raw} 33 | {:error, reason} -> {:error, to_string(reason)} 34 | end 35 | end 36 | 37 | @doc """ 38 | Validates whether a hash matches a given set of data 39 | via a SHA3 function, or returns `:invalid`. 40 | 41 | ## Examples 42 | 43 | iex> ExWire.Crypto.hash_matches("hi mom", <<228, 33, 19, 6, 43, 181, 255, 41, 190, 203, 202, 88, 58, 103, 207, 48, 227, 138, 243, 96, 69, 152, 95, 32, 48, 43, 200, 207, 79, 64, 252, 60>>) 44 | :valid 45 | 46 | iex> ExWire.Crypto.hash_matches("hi mom", <<3>>) 47 | :invalid 48 | """ 49 | @spec hash_matches(binary(), hash) :: :valid | :invalid 50 | def hash_matches(data, check_hash) do 51 | if hash(data) == check_hash do 52 | :valid 53 | else 54 | :invalid 55 | end 56 | end 57 | 58 | @doc """ 59 | Similar to `hash_matches/2`, except raises an error if there 60 | is an invalid hash. 61 | 62 | ## Examples 63 | 64 | iex> ExWire.Crypto.assert_hash("hi mom", <<228, 33, 19, 6, 43, 181, 255, 41, 190, 203, 202, 88, 58, 103, 207, 48, 227, 138, 243, 96, 69, 152, 95, 32, 48, 43, 200, 207, 79, 64, 252, 60>>) 65 | :ok 66 | 67 | iex> ExWire.Crypto.assert_hash("hi mom", <<3>>) 68 | ** (ExWire.Crypto.HashMismatch) Invalid hash 69 | """ 70 | @spec assert_hash(binary(), hash) :: :ok 71 | def assert_hash(data, check_hash) do 72 | case hash_matches(data, check_hash) do 73 | :valid -> :ok 74 | :invalid -> raise HashMismatch, "Invalid hash" 75 | end 76 | end 77 | 78 | @doc """ 79 | Returns the SHA3 hash of a given set of data. 80 | 81 | ## Examples 82 | 83 | iex> ExWire.Crypto.hash("hi mom") 84 | <<228, 33, 19, 6, 43, 181, 255, 41, 190, 203, 202, 88, 58, 103, 207, 85 | 48, 227, 138, 243, 96, 69, 152, 95, 32, 48, 43, 200, 207, 79, 64, 86 | 252, 60>> 87 | 88 | iex> ExWire.Crypto.hash("hi dad") 89 | <<239, 144, 71, 138, 41, 74, 120, 227, 61, 182, 176, 178, 193, 220, 90 | 118, 58, 85, 199, 164, 53, 22, 64, 16, 14, 145, 25, 92, 250, 124, 91 | 174, 44, 234>> 92 | 93 | iex> ExWire.Crypto.hash("") 94 | <<197, 210, 70, 1, 134, 247, 35, 60, 146, 126, 125, 178, 220, 199, 3, 95 | 192, 229, 0, 182, 83, 202, 130, 39, 59, 123, 250, 216, 4, 93, 133, 96 | 164, 112>> 97 | """ 98 | @spec hash(binary()) :: hash 99 | def hash(data) do 100 | ExthCrypto.Hash.Keccak.kec(data) 101 | end 102 | 103 | end -------------------------------------------------------------------------------- /lib/ex_wire/discovery.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Discovery do 2 | @moduledoc """ 3 | Discovery is responsible for discovering a number of neighbors 4 | using the RLPx Discovery Protocol. 5 | """ 6 | 7 | use GenServer 8 | 9 | require Logger 10 | 11 | alias ExWire.Struct.Neighbour 12 | 13 | @min_neighbours 50 14 | 15 | @doc """ 16 | Starts a Discovery server. 17 | """ 18 | def start_link(bootnodes) do 19 | GenServer.start_link(__MODULE__, bootnodes, name: __MODULE__) 20 | end 21 | 22 | @doc """ 23 | Once we start a Discovery server with bootnodes, we'll connect 24 | and ask them for neighbours. 25 | """ 26 | def init(nodes) do 27 | # Configure how we describe ourselves 28 | local_endpoint = if ExWire.Config.use_nat() do 29 | # Use a NAT for inbound ports 30 | {:ok, context} = :nat_upnp.discover() 31 | 32 | # TODO: Should we remove existing mapping? 33 | # :nat_upnp.delete_port_mapping(context, :udp, ExWire.Config.listen_port()) 34 | 35 | {_, _, ip_address_binary} = context 36 | {:ok, ip_address} = :inet.parse_address(ip_address_binary) 37 | 38 | :ok = :nat_upnp.add_port_mapping(context, :udp, ExWire.Config.listen_port(), ExWire.Config.listen_port(), 'discovery mapping', 0) 39 | 40 | %ExWire.Struct.Endpoint{ 41 | ip: ip_address, 42 | udp_port: ExWire.Config.listen_port(), 43 | tcp_port: ExWire.Config.listen_port() 44 | } 45 | else 46 | # Use defaults 47 | %ExWire.Struct.Endpoint{ 48 | ip: ExWire.Config.local_ip(), 49 | udp_port: ExWire.Config.listen_port(), 50 | tcp_port: ExWire.Config.listen_port() 51 | } 52 | end 53 | 54 | :timer.sleep(1000) 55 | 56 | neighbours = for node <- nodes do 57 | {:ok, neighbour} = ExWire.Struct.Neighbour.from_uri(node) 58 | 59 | ping_neighbour(neighbour, local_endpoint) 60 | 61 | neighbour 62 | end 63 | 64 | {:ok, %{ 65 | neighbours: neighbours, 66 | local_endpoint: local_endpoint 67 | }} 68 | end 69 | 70 | def handle_cast({:ping, node_id}, state) do 71 | Logger.debug("[Discovery] Received ping to #{node_id |> ExthCrypto.Math.bin_to_hex}") 72 | 73 | # For now, do nothing. 74 | 75 | {:noreply, state} 76 | end 77 | 78 | def handle_cast({:pong, node_id}, state=%{neighbours: neighbours}) do 79 | Logger.debug("[Discovery] Received pong from #{node_id |> ExthCrypto.Math.bin_to_hex}") 80 | 81 | # If we get a pong and we like it, we should connect via TCP. 82 | case Enum.find(neighbours, fn neighbour -> neighbour.node == node_id end) do 83 | nil -> Logger.debug("[Discovery] Ignoring pong, unknown node..") 84 | neighbour -> 85 | Logger.debug("[Discovery] Got pong from known peer, connecting via TCP.") 86 | find_neighbours(neighbour) 87 | ExWire.PeerSupervisor.connect(neighbour) 88 | end 89 | 90 | {:noreply, state} 91 | end 92 | 93 | def handle_cast({:add_neighbours, add_neighbours}, state=%{neighbours: neighbours, local_endpoint: local_endpoint}) do 94 | # If these are new nodes, we should ping them to see round-trip 95 | # time. If we like the neighbour, we can try and establish a 96 | # RLPx connection. 97 | known_nodes = for neighbour <- neighbours, do: neighbour.node 98 | 99 | new_neighbours = Enum.filter(add_neighbours.nodes, fn neighbour -> 100 | neighbour.node != ExWire.Config.node_id() 101 | and not Enum.member?(known_nodes, neighbour.node) 102 | end) 103 | 104 | Logger.debug("[Discovery] Hi-dilly-ho received #{Enum.count(add_neighbours.nodes)} neighboureenos, #{Enum.count(new_neighbours)} newerific") 105 | 106 | # For each new neighbour, send a ping 107 | for neighbour <- new_neighbours do 108 | ping_neighbour(neighbour, local_endpoint) 109 | 110 | if Enum.count(neighbours) < @min_neighbours do 111 | find_neighbours(neighbour) 112 | end 113 | end 114 | 115 | total_neighbours = neighbours ++ new_neighbours 116 | 117 | Logger.debug("[Discovery] Neighbour Count: #{Enum.count(total_neighbours)}") 118 | 119 | {:noreply, Map.put(state, :neighbours, total_neighbours)} 120 | end 121 | 122 | def handle_call({:get_neighbours, _target}, _from, state=%{neighbours: neighbours}) do 123 | {:reply, neighbours, state} 124 | end 125 | 126 | @doc """ 127 | Informs us that we decided to ping a node. We are interested 128 | in this so that we can track the round-trip time. 129 | """ 130 | @spec ping(pid(), ExWire.node_id) :: :ok 131 | def ping(pid, node_id) do 132 | GenServer.cast(pid, {:ping, node_id}) 133 | end 134 | 135 | @doc """ 136 | Informs us that a node has responded to a ping. This is important since 137 | we may decide to ask this node for neighbours. 138 | """ 139 | @spec pong(pid(), ExWire.node_id) :: :ok 140 | def pong(pid, node_id) do 141 | GenServer.cast(pid, {:pong, node_id}) 142 | end 143 | 144 | @doc """ 145 | Informs us that we have been told of the existence 146 | of these neighbours. We will ping new neighbours before 147 | adding them to our list. 148 | """ 149 | @spec add_neighbours(pid(), [Neighbour.t]) :: :ok 150 | def add_neighbours(pid, neighbours) do 151 | GenServer.cast(pid, {:add_neighbours, neighbours}) 152 | end 153 | 154 | @doc """ 155 | Asks for neighbours. If target is given, we'll try to find 156 | neighbours close to that target. 157 | """ 158 | @spec get_neighbours(pid()) :: [Neighbour.t] 159 | def get_neighbours(pid, target \\ nil) do 160 | GenServer.call(pid, {:get_neighbours, target}) 161 | end 162 | 163 | defp ping_neighbour(neighbour, local_endpoint) do 164 | Logger.debug("[Discovery] Initiating ping to #{inspect neighbour, limit: :infinity}, #{inspect local_endpoint, limit: :infinity}") 165 | 166 | # Send a ping to each node 167 | ping = %ExWire.Message.Ping{ 168 | version: 1, 169 | from: local_endpoint, 170 | to: neighbour.endpoint, 171 | timestamp: ExWire.Util.Timestamp.soon(), 172 | } 173 | 174 | ExWire.Network.send(ping, ExWire.Adapter.UDP, neighbour.endpoint) 175 | end 176 | 177 | defp find_neighbours(neighbour) do 178 | # Logger.debug("[Discovery] Initiating find neighbours to #{inspect neighbour, limit: :infinity}") 179 | 180 | # Ask node for neighbours 181 | find_neighbours = %ExWire.Message.FindNeighbours{ 182 | target: ExthCrypto.Math.nonce(64), # random target address 183 | timestamp: ExWire.Util.Timestamp.soon(), 184 | } 185 | 186 | ExWire.Network.send(find_neighbours, ExWire.Adapter.UDP, neighbour.endpoint) 187 | end 188 | end -------------------------------------------------------------------------------- /lib/ex_wire/exth.ex: -------------------------------------------------------------------------------- 1 | defmodule Exth do 2 | @moduledoc """ 3 | General helper functions, like for inspection. 4 | """ 5 | 6 | @spec inspect(any(), String.t | nil) :: any() 7 | def inspect(variable, prefix \\ nil) do 8 | args = if prefix, do: [prefix, variable], else: variable 9 | 10 | IO.inspect(args, limit: :infinity) 11 | 12 | variable 13 | end 14 | end -------------------------------------------------------------------------------- /lib/ex_wire/framing/frame.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Framing.Frame do 2 | @moduledoc """ 3 | Handles framing a message for transport in RLPx. 4 | 5 | This is defined in the [RLPx docs[(https://github.com/ethereum/devp2p/blob/master/rlpx.md) 6 | under Framing section. 7 | 8 | TODO: Handle multi-frame packets, etc. 9 | TODO: Add tests, etc. 10 | """ 11 | 12 | alias ExthCrypto.MAC 13 | alias ExthCrypto.AES 14 | alias ExWire.Framing.Secrets 15 | 16 | @type frame :: binary() 17 | 18 | @spec frame(integer(), ExRLP.t, Secrets.t) :: {frame, Secrets.t} 19 | def frame(packet_type, packet_data, frame_secrets=%Secrets{egress_mac: egress_mac, encoder_stream: encoder_stream, mac_encoder: mac_encoder, mac_secret: mac_secret}) do 20 | # frame: 21 | # normal: rlp(packet-type) [|| rlp(packet-data)] || padding 22 | # chunked-0: rlp(packet-type) || rlp(packet-data...) 23 | # chunked-n: rlp(...packet-data) || padding 24 | # padding: zero-fill to 16-byte boundary (only necessary for last frame) 25 | frame_unpadded = 26 | ExRLP.encode(packet_type) <> (if packet_data, do: ExRLP.encode(packet_data), else: <<>>) 27 | 28 | # frame-size: 3-byte integer size of frame, big endian encoded (excludes padding) 29 | frame_size_int = byte_size(frame_unpadded) 30 | frame_size = <> 31 | 32 | # assert! total-packet-size: < 2**32 33 | frame_padding = padding_for(frame_size_int, 16) 34 | 35 | # header-data: 36 | # normal: rlp.list(protocol-type[, context-id]) 37 | # chunked-0: rlp.list(protocol-type, context-id, total-packet-size) 38 | # chunked-n: rlp.list(protocol-type, context-id) 39 | # values: 40 | # protocol-type: < 2**16 41 | # context-id: < 2**16 (optional for normal frames) 42 | # total-packet-size: < 2**32 43 | # protocol_type = <<>> 44 | # context_id = <<>> 45 | # header_data = [protocol_type, context_id] |> ExRLP.encode() 46 | header_data = <<0xc2, 0x80, 0x80>> # Honestly, this is what Geth and Parity use as a header data. 47 | header_padding = padding_for(byte_size(frame_size <> header_data), 16) 48 | 49 | # header: frame-size || header-data || padding 50 | header = frame_size <> header_data <> header_padding 51 | 52 | {encoder_stream, header_enc} = AES.stream_encrypt(header, encoder_stream) 53 | 54 | # header-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest 55 | egress_mac = update_mac(egress_mac, mac_encoder, mac_secret, header_enc) 56 | header_mac = MAC.final(egress_mac) |> Binary.take(16) 57 | 58 | {encoder_stream, frame_unpadded_enc} = AES.stream_encrypt(frame_unpadded, encoder_stream) 59 | 60 | {encoder_stream, frame_padding_enc} = if byte_size(frame_padding) > 0 do 61 | AES.stream_encrypt(frame_padding, encoder_stream) 62 | else 63 | {encoder_stream, <<>>} 64 | end 65 | 66 | frame_enc = frame_unpadded_enc <> frame_padding_enc 67 | 68 | # frame-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ right128(egress-mac.update(frame-ciphertext).digest)) 69 | # from EncryptedConnection::update_mac(&mut self.egress_mac, &mut self.mac_encoder, &[0u8; 0]); 70 | egress_mac = MAC.update(egress_mac, frame_enc) 71 | egress_mac = update_mac(egress_mac, mac_encoder, mac_secret, nil) 72 | frame_mac = MAC.final(egress_mac) |> Binary.take(16) 73 | 74 | # egress-mac: h256, continuously updated with egress-bytes* 75 | # ingress-mac: h256, continuously updated with ingress-bytes* 76 | 77 | # Single-frame packet: 78 | # header || header-mac || frame || frame-mac 79 | frame = header_enc <> header_mac <> frame_enc <> frame_mac 80 | 81 | # Return packet and secrets with updated egress mac and symmetric encoder 82 | {frame, %{frame_secrets | egress_mac: egress_mac, encoder_stream: encoder_stream}} 83 | end 84 | 85 | @spec unframe(binary(), Secrets.t) :: {:ok, integer(), binary(), binary(), Secrets.t} | {:error, String.t} 86 | def unframe(frame, frame_secrets=%Secrets{ingress_mac: ingress_mac, decoder_stream: decoder_stream, mac_encoder: mac_encoder, mac_secret: mac_secret}) do 87 | << 88 | header_enc::binary-size(16), # is header always 128 bits? 89 | header_mac::binary-size(16), 90 | frame_rest::binary() 91 | >> = frame 92 | 93 | # verify header mac 94 | ingress_mac = update_mac(ingress_mac, mac_encoder, mac_secret, header_enc) 95 | expected_header_mac = MAC.final(ingress_mac) |> Binary.take(16) 96 | 97 | if expected_header_mac != header_mac do 98 | {:error, "Failed to match header ingress mac"} 99 | else 100 | {decoder_stream, header} = AES.stream_decrypt(header_enc, decoder_stream) 101 | 102 | << 103 | frame_size::integer-size(24), 104 | _header_data_and_padding::binary() 105 | >> = header 106 | 107 | # TODO: We should read the header? But, it's unused by all clients. 108 | # header_rlp = header_data_and_padding |> ExRLP.decode 109 | # protocol_id = Enum.at(header_rlp, 0) |> ExRLP.decode 110 | 111 | frame_padding_bytes = padding_size(frame_size, 16) 112 | 113 | if byte_size(frame_rest) < frame_size + frame_padding_bytes + 16 do 114 | {:error, "Insufficent data"} 115 | else 116 | 117 | # let's go and ignore the entire header data.... 118 | << 119 | frame_enc::binary-size(frame_size), 120 | frame_padding::binary-size(frame_padding_bytes), 121 | frame_mac::binary-size(16), 122 | frame_rest::binary() 123 | >> = frame_rest 124 | 125 | frame_enc_with_padding = frame_enc <> frame_padding 126 | 127 | ingress_mac = MAC.update(ingress_mac, frame_enc_with_padding) 128 | ingress_mac = update_mac(ingress_mac, mac_encoder, mac_secret, nil) 129 | expected_frame_mac = MAC.final(ingress_mac) |> Binary.take(16) 130 | 131 | if expected_frame_mac != frame_mac do 132 | {:error, "Failed to match frame ingress mac"} 133 | else 134 | {decoder_stream, frame_with_padding} = AES.stream_decrypt(frame_enc_with_padding, decoder_stream) 135 | 136 | << 137 | frame::binary-size(frame_size), 138 | _frame_padding::binary() 139 | >> = frame_with_padding 140 | 141 | << 142 | packet_type_rlp::binary-size(1), 143 | packet_data_rlp::binary() 144 | >> = frame 145 | 146 | { 147 | :ok, 148 | packet_type_rlp |> ExRLP.decode |> :binary.decode_unsigned, 149 | packet_data_rlp |> ExRLP.decode, 150 | frame_rest, 151 | %{frame_secrets | ingress_mac: ingress_mac, decoder_stream: decoder_stream} 152 | } 153 | end 154 | end 155 | end 156 | end 157 | 158 | # updateMAC reseeds the given hash with encrypted seed. 159 | # it returns the first 16 bytes of the hash sum after seeding. 160 | @spec update_mac(MAC.mac_inst, ExthCrypto.Cipher.cipher, ExthCrypto.Key.symmetric_key, binary()) :: MAC.mac_inst 161 | defp update_mac(mac, mac_encoder, mac_secret, seed) do 162 | final = MAC.final(mac) |> Binary.take(16) 163 | 164 | enc = ExthCrypto.Cipher.encrypt(final, mac_secret, mac_encoder) |> Binary.take(-16) 165 | 166 | enc_xored = ExthCrypto.Math.xor(enc, (if seed, do: seed, else: final)) 167 | 168 | MAC.update(mac, enc_xored) 169 | end 170 | 171 | @spec padding_size(integer(), integer()) :: integer() 172 | defp padding_size(given_size, to_size) do 173 | ( to_size - rem(given_size, to_size) ) 174 | end 175 | 176 | @spec padding_for(integer(), integer()) :: binary() 177 | defp padding_for(given_size, to_size) do 178 | padding_bits = padding_size(given_size, to_size) * 8 179 | 180 | <<0::size(padding_bits)>> 181 | end 182 | 183 | end -------------------------------------------------------------------------------- /lib/ex_wire/framing/secrets.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Framing.Secrets do 2 | @moduledoc """ 3 | Secrets are used to both encrypt and authenticate incoming 4 | and outgoing peer to peer messages. 5 | """ 6 | 7 | alias ExthCrypto.AES 8 | alias ExthCrypto.MAC 9 | alias ExthCrypto.Hash.Keccak 10 | 11 | @type t :: %__MODULE__{ 12 | egress_mac: MAC.mac_inst, 13 | ingress_mac: MAC.mac_inst, 14 | mac_encoder: ExthCrypto.Cipher.cipher, 15 | mac_secret: ExthCrypto.Key.symmetric_key, 16 | encoder_stream: ExthCrypto.Cipher.stream, 17 | decoder_stream: ExthCrypto.Cipher.stream, 18 | token: binary() 19 | } 20 | 21 | defstruct [ 22 | :egress_mac, 23 | :ingress_mac, 24 | :mac_encoder, 25 | :mac_secret, 26 | :encoder_stream, 27 | :decoder_stream, 28 | :token 29 | ] 30 | 31 | @spec new(MAC.mac_inst, MAC.mac_inst, ExthCrypto.Key.symmetric_key, ExthCrypto.Key.symmetric_key, binary()) :: t 32 | def new(egress_mac, ingress_mac, mac_secret, symmetric_key, token) do 33 | # initialize AES stream with empty init_vector 34 | encoder_stream = AES.stream_init(:ctr, symmetric_key, <<0::size(128)>>) 35 | decoder_stream = AES.stream_init(:ctr, symmetric_key, <<0::size(128)>>) 36 | mac_encoder = {AES, AES.block_size, :ecb} 37 | 38 | %__MODULE__{ 39 | egress_mac: egress_mac, 40 | ingress_mac: ingress_mac, 41 | mac_encoder: mac_encoder, 42 | mac_secret: mac_secret, 43 | encoder_stream: encoder_stream, 44 | decoder_stream: decoder_stream, 45 | token: token 46 | } 47 | end 48 | 49 | @doc """ 50 | After a handshake has been completed (i.e. auth and ack have been exchanged), 51 | we're ready to derive the secrets to be used to encrypt frames. This function 52 | performs the required computation. 53 | 54 | # TODO: Add examples 55 | # TODO: Clean up API interface 56 | """ 57 | @spec derive_secrets(boolean(), ExthCrypto.Key.private_key, ExthCrypto.Key.public_key, binary(), binary(), binary(), binary()) :: t 58 | def derive_secrets(is_initiator, my_ephemeral_private_key, remote_ephemeral_public_key, remote_nonce, my_nonce, auth_data, ack_data) do 59 | remote_ephemeral_public_key_raw = remote_ephemeral_public_key |> ExthCrypto.Key.raw_to_der 60 | 61 | ephemeral_shared_secret = ExthCrypto.ECIES.ECDH.generate_shared_secret(my_ephemeral_private_key, remote_ephemeral_public_key_raw) 62 | 63 | # TODO: Nonces will need to be reversed come winter 64 | shared_secret = Keccak.kec(ephemeral_shared_secret <> Keccak.kec(remote_nonce <> my_nonce)) 65 | 66 | # `token` can be used to resume a connection with a minimal handshake 67 | token = Keccak.kec(shared_secret) 68 | 69 | aes_secret = Keccak.kec(ephemeral_shared_secret <> shared_secret) 70 | mac_secret = Keccak.kec(ephemeral_shared_secret <> aes_secret) 71 | 72 | mac_1 = 73 | MAC.init(:kec) 74 | |> MAC.update(ExthCrypto.Math.xor(mac_secret, remote_nonce)) 75 | |> MAC.update(auth_data) 76 | 77 | mac_2 = MAC.init(:kec) 78 | |> MAC.update(ExthCrypto.Math.xor(mac_secret, my_nonce)) 79 | |> MAC.update(ack_data) 80 | 81 | {egress_mac, ingress_mac} = if is_initiator do 82 | {mac_1, mac_2} 83 | else 84 | {mac_2, mac_1} 85 | end 86 | 87 | __MODULE__.new( 88 | egress_mac, 89 | ingress_mac, 90 | mac_secret, 91 | aes_secret, 92 | token 93 | ) 94 | end 95 | end -------------------------------------------------------------------------------- /lib/ex_wire/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler do 2 | @moduledoc """ 3 | Defines a behavior for all message handlers of RLPx messages. 4 | 5 | Message handlers tell us how we should respond to a given incoming transmission, 6 | after it has been decoded. 7 | """ 8 | 9 | alias ExWire.Message 10 | alias ExWire.Crypto 11 | 12 | require Logger 13 | 14 | @handlers %{ 15 | 0x01 => ExWire.Handler.Ping, 16 | 0x02 => ExWire.Handler.Pong, 17 | 0x03 => ExWire.Handler.FindNeighbours, 18 | 0x04 => ExWire.Handler.Neighbours, 19 | } 20 | 21 | defmodule Params do 22 | @moduledoc "Struct to store parameters from an incoming message" 23 | 24 | defstruct [ 25 | remote_host: nil, 26 | signature: nil, 27 | recovery_id: nil, 28 | hash: nil, 29 | type: nil, 30 | data: nil, 31 | timestamp: nil, 32 | node_id: nil 33 | ] 34 | 35 | @type t :: %__MODULE__{ 36 | remote_host: ExWire.Struct.Endpoint.t, 37 | signature: Crpyto.signature, 38 | recovery_id: Crypto.recovery_id, 39 | hash: Crypto.hash, 40 | type: integer(), 41 | data: binary(), 42 | timestamp: integer(), 43 | node_id: ExWire.node_id, 44 | } 45 | end 46 | 47 | @type handler_response :: :not_implented | :no_response | {:respond, Message.t} 48 | @callback handle(Params.t) :: handler_response 49 | 50 | @doc """ 51 | Decides which module to route the given message to, 52 | or returns `:not_implemented` if we have no implemented 53 | a handler for the message type. 54 | 55 | ## Examples 56 | 57 | iex> ExWire.Handler.dispatch(0x01, %ExWire.Handler.Params{ 58 | ...> remote_host: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: 55}, 59 | ...> signature: 2, 60 | ...> recovery_id: 3, 61 | ...> hash: <<5>>, 62 | ...> data: [1, [<<1,2,3,4>>, <<>>, <<5>>], [<<5,6,7,8>>, <<6>>, <<>>], 4] |> ExRLP.encode(), 63 | ...> timestamp: 123, 64 | ...> }, nil) 65 | {:respond, %ExWire.Message.Pong{ 66 | hash: <<5>>, 67 | timestamp: 123, 68 | to: %ExWire.Struct.Endpoint{ 69 | ip: {1, 2, 3, 4}, 70 | tcp_port: 5, 71 | udp_port: nil 72 | } 73 | }} 74 | 75 | iex> ExWire.Handler.dispatch(0x99, %ExWire.Handler.Params{}, nil) 76 | :not_implemented 77 | 78 | # TODO: Add a `no_response` test case 79 | """ 80 | @spec dispatch(integer(), Params.t, identifier() | nil) :: handler_response 81 | def dispatch(type, params, discovery) do 82 | case @handlers[type] do 83 | nil -> 84 | Logger.warn("Message code `#{inspect type, base: :hex}` not implemented") 85 | :not_implemented 86 | mod when is_atom(mod) -> apply(mod, :handle, [params, discovery]) 87 | end 88 | end 89 | 90 | end -------------------------------------------------------------------------------- /lib/ex_wire/handler/find_neighbours.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler.FindNeighbours do 2 | @moduledoc """ 3 | Not currently implemented. 4 | """ 5 | 6 | alias ExWire.Handler 7 | 8 | @doc """ 9 | Handler for a FindNeighbors message. 10 | 11 | ## Examples 12 | 13 | iex> ExWire.Handler.FindNeighbours.handle(%ExWire.Handler.Params{ 14 | ...> remote_host: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: 55}, 15 | ...> signature: 2, 16 | ...> recovery_id: 3, 17 | ...> hash: <<5>>, 18 | ...> data: <<194, 1, 2>>, 19 | ...> timestamp: 7, 20 | ...> }, nil) 21 | {:respond, %ExWire.Message.Neighbours{ 22 | nodes: [], 23 | timestamp: 7, 24 | }} 25 | """ 26 | @spec handle(Handler.Params.t, identifier | nil) :: Handler.handler_response 27 | def handle(params, discovery) do 28 | find_neighbours = ExWire.Message.FindNeighbours.decode(params.data) 29 | 30 | nodes = if discovery do 31 | ExWire.Discovery.get_neighbours(discovery, find_neighbours.target) 32 | else 33 | [] 34 | end 35 | 36 | {:respond, %ExWire.Message.Neighbours{ 37 | nodes: nodes, 38 | timestamp: params.timestamp, 39 | }} 40 | end 41 | 42 | end -------------------------------------------------------------------------------- /lib/ex_wire/handler/neighbours.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler.Neighbours do 2 | @moduledoc """ 3 | Module to handle a response to a Neighbours message, which 4 | should be to add the neighbors to the correct K-Buckets. 5 | 6 | Jim Nabors is way cool. 7 | """ 8 | 9 | require Logger 10 | 11 | alias ExWire.Handler 12 | alias ExWire.Message.Neighbours 13 | 14 | @doc """ 15 | Handler for a Neighbours message. 16 | 17 | ## Examples 18 | 19 | iex> message = %ExWire.Message.Neighbours{ 20 | ...> nodes: [ 21 | ...> %ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, node: <<7, 7>>}, 22 | ...> %ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, node: <<8, 8>>}], 23 | ...> timestamp: 1 24 | ...> } 25 | iex> ExWire.Handler.Neighbours.handle(%ExWire.Handler.Params{ 26 | ...> remote_host: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: 55}, 27 | ...> signature: 2, 28 | ...> recovery_id: 3, 29 | ...> hash: <<5>>, 30 | ...> data: message |> ExWire.Message.Neighbours.encode(), 31 | ...> timestamp: 123, 32 | ...> }, nil) 33 | :no_response 34 | """ 35 | @spec handle(Handler.Params.t, identifier() | nil) :: Handler.handler_response 36 | def handle(params, discovery) do 37 | neighbours = Neighbours.decode(params.data) 38 | 39 | if discovery, do: ExWire.Discovery.add_neighbours(discovery, neighbours) 40 | 41 | :no_response 42 | end 43 | 44 | end -------------------------------------------------------------------------------- /lib/ex_wire/handler/ping.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler.Ping do 2 | @moduledoc """ 3 | Module to handle a respond to a Ping message, which generate a Pong response. 4 | """ 5 | 6 | alias ExWire.Handler 7 | alias ExWire.Message.Pong 8 | alias ExWire.Message.Ping 9 | 10 | @doc """ 11 | Handler for a Ping message. 12 | 13 | ## Examples 14 | 15 | iex> ExWire.Handler.Ping.handle(%ExWire.Handler.Params{ 16 | ...> remote_host: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: 55}, 17 | ...> signature: 2, 18 | ...> recovery_id: 3, 19 | ...> hash: <<5>>, 20 | ...> data: [1, [<<1,2,3,4>>, <<>>, <<5>>], [<<5,6,7,8>>, <<6>>, <<>>], 4] |> ExRLP.encode(), 21 | ...> timestamp: 123, 22 | ...> }, nil) 23 | {:respond, %ExWire.Message.Pong{ 24 | hash: <<5>>, 25 | timestamp: 123, 26 | to: %ExWire.Struct.Endpoint{ 27 | ip: {1, 2, 3, 4}, 28 | tcp_port: 5, 29 | udp_port: nil 30 | } 31 | }} 32 | """ 33 | @spec handle(Handler.Params.t, identifier() | nil) :: Handler.handler_response 34 | def handle(params, _discovery) do 35 | ping = Ping.decode(params.data) 36 | 37 | {:respond, %Pong{ 38 | to: ping.from, 39 | hash: params.hash, 40 | timestamp: params.timestamp, 41 | }} 42 | end 43 | 44 | end -------------------------------------------------------------------------------- /lib/ex_wire/handler/pong.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler.Pong do 2 | @moduledoc """ 3 | Module to handle a response to a Pong message, which is to do nothing. 4 | """ 5 | 6 | alias ExWire.Handler 7 | alias ExWire.Message.Pong 8 | 9 | @doc """ 10 | Handler for a Pong message. 11 | 12 | ## Examples 13 | 14 | iex> ExWire.Handler.Pong.handle(%ExWire.Handler.Params{ 15 | ...> remote_host: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: 55}, 16 | ...> signature: 2, 17 | ...> recovery_id: 3, 18 | ...> hash: <<5>>, 19 | ...> data: [[<<1,2,3,4>>, <<>>, <<5>>], <<2>>, 3] |> ExRLP.encode(), 20 | ...> timestamp: 123, 21 | ...> }, nil) 22 | :no_response 23 | """ 24 | @spec handle(Handler.Params.t, identifier() | nil) :: Handler.handler_response 25 | def handle(params, discovery) do 26 | _pong = Pong.decode(params.data) 27 | 28 | if discovery, do: ExWire.Discovery.pong(discovery, params.node_id) 29 | 30 | :no_response 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /lib/ex_wire/handshake/eip_8.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake.EIP8 do 2 | @moduledoc """ 3 | Handles wrapping and unwrapping messages according to the specification in 4 | [Ethereum EIP-8](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md). 5 | 6 | TODO: How do we handle random padding? 7 | """ 8 | 9 | require Logger 10 | 11 | # Amount of bytes added when encrypting with ECIES. 12 | # EIP Question: This is magic, isn't it? Definitely magic. 13 | @ecies_overhead 113 14 | @protocol_version 4 15 | 16 | @doc """ 17 | Wraps a message in EIP-8 encoding, according to 18 | [Ethereum EIP-8](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md). 19 | 20 | ## Examples 21 | 22 | iex> {:ok, bin} = ExWire.Handshake.EIP8.wrap_eip_8(["jedi", "knight"], ExthCrypto.Test.public_key, "1.2.3.4", ExthCrypto.Test.key_pair(:key_b), ExthCrypto.Test.init_vector) 23 | iex> bin |> ExthCrypto.Math.bin_to_hex 24 | "00e6049871eb081567823267592abac8ec9e9fddfdece7901a15f233b53f304d7860686c21601ba1a7f56680e22d0ac03eccd08e496469514c25ae1d5e55f391c1956f0102030405060708090a0b0c0d0e0f102cb1de6abaaa6f731dbe4cd77135af3c6c49a8a065db5017e108aebc6db886a1f242e876982f69985e62412d240107652d4a78e5d7e3989d74fd7f97b3c4a34d2736ee8a912f7ea23c3327f0ed9b9d15b7999644b6e00a440eebc24da9dabb6412f4c6573d2a18c6678ad689e3b1849a33d0fa1c7ffb43a4033428646258196942e611ea2bf31b983e98356f2f57951c4aebb8dd54" 25 | """ 26 | @spec wrap_eip_8(ExRLP.t, ExthCrypto.Key.public_key, binary(), {ExthCrypto.Key.public_key, ExthCrypto.Key.private_key} | nil, Cipher.init_vector | nil) :: {:ok, binary()} | {:error, String.t} 27 | def wrap_eip_8(rlp, her_static_public_key, remote_addr, my_ephemeral_key_pair \\ nil, init_vector \\ nil) do 28 | Logger.debug("[Network] Sending EIP8 Handshake to #{remote_addr}") 29 | 30 | # According to EIP-8, we add padding to prevent length detection attacks. Thus, it should be 31 | # acceptable to pad with zero instead of random data. We opt for padding with zeros. 32 | padding = ExthCrypto.Math.pad(<<>>, 100) 33 | 34 | # rlp.list(sig, initiator-pubk, initiator-nonce, auth-vsn) 35 | # EIP Question: Why is random appended at the end? Is this going to make it hard to upgrade the protocol? 36 | auth_body = ExRLP.encode(rlp ++ [@protocol_version, padding]) 37 | 38 | # size of enc-auth-body, encoded as a big-endian 16-bit integer 39 | # EIP Question: It's insane we expect the protocol to know the size of the packet prior to encoding. 40 | auth_size_int = byte_size(auth_body) + @ecies_overhead 41 | auth_size = <> 42 | 43 | # ecies.encrypt(recipient-pubk, auth-body, auth-size) 44 | with {:ok, enc_auth_body} <- ExthCrypto.ECIES.encrypt(her_static_public_key, auth_body, <<>>, auth_size, my_ephemeral_key_pair, init_vector) do 45 | # size of enc-auth-body, encoded as a big-endian 16-bit integer 46 | enc_auth_body_size = byte_size(enc_auth_body) 47 | 48 | if enc_auth_body_size != auth_size_int do 49 | # The auth-size is hard coded, so the least we can do is verify 50 | {:error, "Invalid encoded body size"} 51 | else 52 | # auth-packet = auth-size || enc-auth-body 53 | # EIP Question: Doesn't RLP already handle size definitions? 54 | {:ok, auth_size <> enc_auth_body} 55 | end 56 | end 57 | end 58 | 59 | @doc """ 60 | Unwraps a message in EIP-8 encoding, according to 61 | [Ethereum EIP-8](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md). 62 | 63 | ## Examples 64 | 65 | iex> "00e6049871eb081567823267592abac8ec9e9fddfdece7901a15f233b53f304d7860686c21601ba1a7f56680e22d0ac03eccd08e496469514c25ae1d5e55f391c1956f0102030405060708090a0b0c0d0e0f102cb1de6abaaa6f731dbe4cd77135af3c6c48aaa361de5610e901a4b761b588aee253fa658c3a7f8f467b5b36381c197a0d6b5ac6f3c6beba5cd455bc9fe98d621707dcb9a51a4895040a1dcbd1a6a32af7d8d407f2a54c0346a28806e597f52b42a59404697f4e913fd38cd2bfecdac553b1987f1b61049f516053a5a1f8cdc9efae57748d98355864f59037e326e7ec9b2d947580" |> ExthCrypto.Math.hex_to_bin 66 | ...> |> ExWire.Handshake.EIP8.unwrap_eip_8(ExthCrypto.Test.private_key(:key_a), "1.2.3.4") 67 | {:ok, 68 | ["jedi", "knight", <<4>>, 69 | <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 70 | 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 71 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 72 | 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 73 | 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 74 | 98, 99, 100>>], 75 | <<0, 230, 4, 152, 113, 235, 8, 21, 103, 130, 50, 103, 89, 42, 186, 200, 236, 76 | 158, 159, 221, 253, 236, 231, 144, 26, 21, 242, 51, 181, 63, 48, 77, 120, 77 | 96, 104, 108, 33, 96, 27, 161, 167, 245, 102, 128, 226, 45, 10, 192, 62, 78 | 204, 208, 142, 73, 100, 105, 81, 76, 37, 174, 29, 94, 85, 243, 145, 193, 79 | 149, 111, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 44, 177, 80 | 222, 106, 186, 170, 111, 115, 29, 190, 76, 215, 113, 53, 175, 60, 108, 72, 81 | 170, 163, 97, 222, 86, 16, 233, 1, 164, 183, 97, 181, 136, 174, 226, 83, 82 | 250, 101, 140, 58, 127, 143, 70, 123, 91, 54, 56, 28, 25, 122, 13, 107, 90, 83 | 198, 243, 198, 190, 186, 92, 212, 85, 188, 159, 233, 141, 98, 23, 7, 220, 84 | 185, 165, 26, 72, 149, 4, 10, 29, 203, 209, 166, 163, 42, 247, 216, 212, 7, 85 | 242, 165, 76, 3, 70, 162, 136, 6, 229, 151, 245, 43, 66, 165, 148, 4, 105, 86 | 127, 78, 145, 63, 211, 140, 210, 191, 236, 218, 197, 83, 177, 152, 127, 27, 87 | 97, 4, 159, 81, 96, 83, 165, 161, 248, 205, 201, 239, 174, 87, 116, 141, 88 | 152, 53, 88, 100, 245, 144, 55, 227, 38, 231, 236, 155, 45, 148, 117, 128>>, 89 | ""} 90 | """ 91 | @spec unwrap_eip_8(binary(), ExthCrypto.Key.private_key, binary()) :: {:ok, RLP.t, binary(), binary()} | {:error, String.t} 92 | def unwrap_eip_8(encoded_packet, my_static_private_key, remote_addr) do 93 | Logger.debug("[Network] Received EIP8 Handshake from #{remote_addr}") 94 | <> = encoded_packet 95 | 96 | case encoded_packet do 97 | <> -> 98 | with {:ok, rlp_bin} <- ExthCrypto.ECIES.decrypt(my_static_private_key, ecies_encoded_message, <<>>, auth_size) do 99 | rlp = ExRLP.decode(rlp_bin) 100 | 101 | 102 | {:ok, rlp, auth_size <> ecies_encoded_message, frame_rest} 103 | end 104 | _ -> 105 | {:error, "Invalid encoded packet"} 106 | end 107 | end 108 | end -------------------------------------------------------------------------------- /lib/ex_wire/handshake/handshake.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake do 2 | @moduledoc """ 3 | Implements the RLPx ECIES handshake protocol. 4 | 5 | This handshake is the first thing that happens after establishing a connection. 6 | Afterwards, we will do a HELLO and protocol handshake. 7 | 8 | Note: this protocol is not extremely well defined, but you can read up on it here: 9 | 1. https://github.com/ethereum/devp2p/blob/master/rlpx.md 10 | 2. https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md 11 | 3. https://github.com/ethereum/go-ethereum/wiki/RLPx-Encryption 12 | 4. https://github.com/ethereum/wiki/wiki/%C3%90%CE%9EVp2p-Wire-Protocol 13 | 5. https://github.com/ethereum/wiki/wiki/Ethereum-Wire-Protocol 14 | """ 15 | 16 | require Logger 17 | 18 | alias ExthCrypto.ECIES.ECDH 19 | alias ExWire.Handshake.EIP8 20 | alias ExWire.Handshake.Struct.AuthMsgV4 21 | alias ExWire.Handshake.Struct.AckRespV4 22 | alias ExWire.Framing.Secrets 23 | 24 | @type token :: binary() 25 | 26 | defmodule Handshake do 27 | defstruct [ 28 | :initiator, 29 | :remote_id, 30 | :remote_pub, # ecdhe-random 31 | :init_nonce, # nonce 32 | :resp_nonce, # 33 | :random_priv_key, # ecdhe-random 34 | :remote_random_pub, # ecdhe-random-pubk 35 | ] 36 | 37 | @type t :: %__MODULE__{ 38 | initiator: boolean(), 39 | remote_id: ExWire.node_id, 40 | remote_pub: ExWire.Config.private_key, 41 | init_nonce: binary(), 42 | resp_nonce: binary(), 43 | random_priv_key: ExWire.Config.private_key, 44 | remote_random_pub: ExWire.Config.pubic_key, 45 | } 46 | end 47 | 48 | @nonce_len 32 49 | 50 | @doc """ 51 | Reads a given auth message, transported during the key initialization phase 52 | of the RLPx protocol. This will generally be handled by the listener of the connection. 53 | 54 | Note: this will handle pre or post-EIP 8 messages. We take a different approach to other 55 | implementations and try EIP-8 first, and if that fails, plain. 56 | """ 57 | @spec read_auth_msg(binary(), ExthCrypto.Key.private_key, String.t) :: {:ok, AuthMsgV4.t, binary()} | {:error, String.t} 58 | def read_auth_msg(encoded_auth, my_static_private_key, remote_addr) do 59 | case EIP8.unwrap_eip_8(encoded_auth, my_static_private_key, remote_addr) do 60 | {:ok, rlp, _bin, frame_rest} -> 61 | # unwrap eip-8 62 | auth_msg = 63 | rlp 64 | |> AuthMsgV4.deserialize() 65 | |> AuthMsgV4.set_remote_ephemeral_public_key(my_static_private_key) 66 | 67 | {:ok, auth_msg, frame_rest} 68 | {:error, _} -> 69 | # unwrap plain 70 | with {:ok, plaintext} <- ExthCrypto.ECIES.decrypt(my_static_private_key, encoded_auth, <<>>, <<>>) do 71 | << 72 | signature::binary-size(65), 73 | _::binary-size(32), 74 | remote_public_key::binary-size(64), 75 | remote_nonce::binary-size(32), 76 | 0x00::size(8) 77 | >> = plaintext 78 | 79 | auth_msg = 80 | [ 81 | signature, 82 | remote_public_key, 83 | remote_nonce, 84 | ExWire.Config.protocol_version() 85 | ] 86 | |> AuthMsgV4.deserialize() 87 | |> AuthMsgV4.set_remote_ephemeral_public_key(my_static_private_key) 88 | 89 | {:ok, auth_msg, <<>>} 90 | end 91 | end 92 | end 93 | 94 | @doc """ 95 | Reads a given ack message, transported during the key initialization phase 96 | of the RLPx protocol. This will generally be handled by the dialer of the connection. 97 | 98 | Note: this will handle pre- or post-EIP 8 messages. We take a different approach to other 99 | implementations and try EIP-8 first, and if that fails, plain. 100 | """ 101 | @spec read_ack_resp(binary(), ExthCrypto.Key.private_key, String.t) :: {:ok, AckRespV4.t, binary(), binary()} | {:error, String.t} 102 | def read_ack_resp(encoded_ack, my_static_private_key, remote_addr) do 103 | case EIP8.unwrap_eip_8(encoded_ack, my_static_private_key, remote_addr) do 104 | {:ok, rlp, ack_resp_bin, frame_rest} -> 105 | # unwrap eip-8 106 | ack_resp = 107 | rlp 108 | |> AckRespV4.deserialize() 109 | 110 | {:ok, ack_resp, ack_resp_bin, frame_rest} 111 | {:error, _reason} -> 112 | # TODO: reason? 113 | 114 | # unwrap plain 115 | with {:ok, plaintext} <- ExthCrypto.ECIES.decrypt(my_static_private_key, encoded_ack, <<>>, <<>>) do 116 | << 117 | remote_ephemeral_public_key::binary-size(64), 118 | remote_nonce::binary-size(32), 119 | 0x00::size(8) 120 | >> = plaintext 121 | 122 | ack_resp = 123 | [ 124 | remote_ephemeral_public_key, 125 | remote_nonce, 126 | ExWire.Config.protocol_version() 127 | ] 128 | |> AckRespV4.deserialize() 129 | 130 | {:ok, ack_resp, encoded_ack, <<>>} 131 | end 132 | end 133 | end 134 | 135 | @doc """ 136 | Builds an AuthMsgV4 which can be serialized and sent over the wire. This will also build an ephemeral key pair 137 | to use during the signing process. 138 | 139 | ## Examples 140 | 141 | iex> {auth_msg_v4, ephemeral_keypair, nonce} = ExWire.Handshake.build_auth_msg(ExthCrypto.Test.public_key(:key_a), ExthCrypto.Test.private_key(:key_a), ExthCrypto.Test.public_key(:key_b), ExthCrypto.Test.init_vector(1, 32), ExthCrypto.Test.key_pair(:key_c)) 142 | iex> %{auth_msg_v4 | signature: nil} # signature will be unique each time 143 | %ExWire.Handshake.Struct.AuthMsgV4{ 144 | remote_ephemeral_public_key: nil, 145 | remote_nonce: <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32>>, 146 | remote_public_key: <<4, 54, 241, 224, 126, 85, 135, 69, 213, 129, 115, 3, 41, 161, 217, 87, 215, 159, 64, 17, 167, 128, 113, 172, 232, 46, 34, 145, 136, 72, 160, 207, 161, 171, 255, 26, 163, 160, 158, 227, 196, 92, 62, 119, 84, 156, 99, 224, 155, 120, 250, 153, 134, 180, 218, 177, 186, 200, 199, 106, 97, 103, 50, 215, 114>>, 147 | remote_version: 63, 148 | signature: nil 149 | } 150 | iex> ephemeral_keypair 151 | { 152 | <<4, 146, 201, 161, 205, 19, 177, 147, 33, 107, 190, 144, 81, 145, 173, 83, 153 | 20, 105, 150, 114, 196, 249, 143, 167, 152, 63, 225, 96, 184, 86, 203, 38, 154 | 134, 241, 40, 152, 74, 34, 68, 233, 204, 91, 240, 208, 254, 62, 169, 53, 155 | 201, 248, 156, 236, 34, 203, 156, 75, 18, 121, 162, 104, 3, 164, 156, 46, 186>>, 156 | <<178, 68, 134, 194, 0, 187, 118, 35, 33, 220, 4, 3, 50, 96, 97, 91, 96, 14, 157 | 71, 239, 7, 102, 33, 187, 194, 221, 152, 36, 95, 22, 121, 48>> 158 | } 159 | iex> nonce 160 | <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32>> 161 | """ 162 | @spec build_auth_msg(ExthCrypto.Key.public_key, ExthCrypto.Key.private_key, ExthCrypto.Key.public_key, binary() | nil, ExthCrypto.Key.key_pair | nil) :: {AuthMsgV4.t, ExthCrypto.Key.key_pair, binary()} 163 | def build_auth_msg(my_static_public_key, my_static_private_key, her_static_public_key, nonce \\ nil, my_ephemeral_keypair \\ nil) do 164 | 165 | # Geneate a random ephemeral keypair 166 | my_ephemeral_keypair = if my_ephemeral_keypair, do: my_ephemeral_keypair, else: ECDH.new_ecdh_keypair() 167 | 168 | {_my_ephemeral_public_key, my_ephemeral_private_key} = my_ephemeral_keypair 169 | 170 | # Determine DH shared secret 171 | shared_secret = ECDH.generate_shared_secret(my_static_private_key, her_static_public_key) 172 | 173 | # Build a nonce unless given 174 | nonce = ( if nonce, do: nonce, else: ExthCrypto.Math.nonce(@nonce_len) ) 175 | 176 | # XOR shared-secret and nonce 177 | shared_secret_xor_nonce = ExthCrypto.Math.xor(shared_secret, nonce) 178 | 179 | # Sign xor'd secret 180 | {signature, _, _, recovery_id} = ExthCrypto.Signature.sign_digest(shared_secret_xor_nonce, my_ephemeral_private_key) 181 | 182 | # Build an auth message to send over the wire 183 | auth_msg = %AuthMsgV4{ 184 | signature: signature <> :binary.encode_unsigned(recovery_id), 185 | remote_public_key: my_static_public_key, 186 | remote_nonce: nonce, 187 | remote_version: ExWire.Config.protocol_version() 188 | } 189 | 190 | # Return auth_msg and my new key pair 191 | {auth_msg, my_ephemeral_keypair, nonce} 192 | end 193 | 194 | @doc """ 195 | Builds a response for an incoming authentication message. 196 | 197 | ## Examples 198 | 199 | iex> ExWire.Handshake.build_ack_resp(ExthCrypto.Test.public_key(:key_c), ExthCrypto.Test.init_vector()) 200 | %ExWire.Handshake.Struct.AckRespV4{ 201 | remote_ephemeral_public_key: <<4, 146, 201, 161, 205, 19, 177, 147, 33, 107, 190, 144, 81, 145, 173, 83, 20, 105, 150, 114, 196, 249, 143, 167, 152, 63, 225, 96, 184, 86, 203, 38, 134, 241, 40, 152, 74, 34, 68, 233, 204, 91, 240, 208, 254, 62, 169, 53, 201, 248, 156, 236, 34, 203, 156, 75, 18, 121, 162, 104, 3, 164, 156, 46, 186>>, 202 | remote_nonce: <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16>>, 203 | remote_version: 63 204 | } 205 | """ 206 | @spec build_ack_resp(ExthCrypto.Key.public_key, binary() | nil) :: AckRespV4.t 207 | def build_ack_resp(remote_ephemeral_public_key, nonce \\ nil) do 208 | # Generate nonce unless given 209 | nonce = if nonce, do: nonce, else: ExthCrypto.Math.nonce(@nonce_len) 210 | 211 | %AckRespV4{ 212 | remote_nonce: nonce, 213 | remote_ephemeral_public_key: remote_ephemeral_public_key, 214 | remote_version: ExWire.Config.protocol_version() 215 | } 216 | end 217 | 218 | @doc """ 219 | Given an incoming message, let's try to accept it as an ack resp. If that works, 220 | we'll derive our secrets from it. 221 | 222 | # TODO: Add examples 223 | """ 224 | @spec try_handle_ack(binary(), binary(), ExthCrypto.Key.private_key, binary(), String.t) :: {:ok, Secrets.t, binary()} | {:invalid, String.t} 225 | def try_handle_ack(ack_data, auth_data, my_ephemeral_private_key, my_nonce, host) do 226 | case ExWire.Handshake.read_ack_resp(ack_data, ExWire.Config.private_key(), host) do 227 | {:ok, %ExWire.Handshake.Struct.AckRespV4{ 228 | remote_ephemeral_public_key: remote_ephemeral_public_key, 229 | remote_nonce: remote_nonce 230 | }, ack_data_limited, frame_rest} -> 231 | # We're the initiator, by definition since we got an ack resp. 232 | secrets = ExWire.Framing.Secrets.derive_secrets( 233 | true, 234 | my_ephemeral_private_key, 235 | remote_ephemeral_public_key, 236 | remote_nonce, 237 | my_nonce, 238 | auth_data, 239 | ack_data_limited 240 | ) 241 | 242 | {:ok, secrets, frame_rest} 243 | {:error, reason} -> 244 | {:invalid, reason} 245 | end 246 | end 247 | 248 | @doc """ 249 | Give an incoming msg, let's try to accept it as an auth msg. If that works, 250 | we'll prepare an ack response to send back and derive our secrets. 251 | 252 | TODO: Add examples 253 | """ 254 | @spec try_handle_auth(binary(), ExthCrypto.Key.key_pair, binary(), binary(), String.t) :: {:ok, binary(), Secrets.t} | {:invalid, String.t} 255 | def try_handle_auth(auth_data, {my_ephemeral_public_key, my_ephemeral_private_key}=my_ephemeral_key_pair, my_nonce, remote_id, host) do 256 | case ExWire.Handshake.read_auth_msg(auth_data, ExWire.Config.private_key(), host) do 257 | {:ok, %ExWire.Handshake.Struct.AuthMsgV4{ 258 | signature: _signature, 259 | remote_public_key: _remote_public_key, 260 | remote_nonce: remote_nonce, 261 | remote_version: remote_version, 262 | remote_ephemeral_public_key: remote_ephemeral_public_key, 263 | }} -> 264 | # First, we'll build an ack, which we'll respond with to the remote peer 265 | ack_resp = ExWire.Handshake.build_ack_resp(remote_ephemeral_public_key: my_ephemeral_public_key, remote_version: remote_version) 266 | 267 | # TODO: Make this accurate 268 | {:ok, encoded_ack_resp} = ack_resp 269 | |> ExWire.Handshake.Struct.AckRespV4.serialize() 270 | |> ExWire.Handshake.EIP8.wrap_eip_8(remote_id, host, my_ephemeral_key_pair) 271 | 272 | # We have the auth, we can derive secrets already 273 | secrets = ExWire.Framing.Secrets.derive_secrets( 274 | false, 275 | my_ephemeral_private_key, 276 | remote_ephemeral_public_key, 277 | remote_nonce, 278 | my_nonce, 279 | auth_data, 280 | encoded_ack_resp 281 | ) 282 | 283 | {:ok, ack_resp, secrets} 284 | {:error, reason} -> 285 | {:invalid, reason} 286 | end 287 | end 288 | 289 | end -------------------------------------------------------------------------------- /lib/ex_wire/handshake/struct/ack_resp_v4.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake.Struct.AckRespV4 do 2 | @moduledoc """ 3 | Simple struct to wrap an auth response. 4 | 5 | The RLPx v4 handshake ack is defined in EIP-8. 6 | """ 7 | 8 | defstruct [ 9 | :remote_ephemeral_public_key, 10 | :remote_nonce, 11 | :remote_version 12 | ] 13 | 14 | @type t :: %__MODULE__{ 15 | remote_ephemeral_public_key: ExthCrypto.Key.public_key, 16 | remote_nonce: binary(), 17 | remote_version: integer(), 18 | } 19 | 20 | @spec serialize(t) :: ExRLP.t 21 | def serialize(auth_resp) do 22 | [ 23 | auth_resp.remote_ephemeral_public_key, 24 | auth_resp.remote_nonce, 25 | auth_resp.remote_version |> :binary.encode_unsigned, 26 | ] 27 | end 28 | 29 | @spec deserialize(ExRLP.t) :: t 30 | def deserialize(rlp) do 31 | [ 32 | remote_ephemeral_public_key | 33 | [remote_nonce | 34 | [remote_version | 35 | _tl 36 | ]]] = rlp 37 | 38 | %__MODULE__{ 39 | remote_ephemeral_public_key: remote_ephemeral_public_key, 40 | remote_nonce: remote_nonce, 41 | remote_version: (if is_binary(remote_version), do: :binary.decode_unsigned(remote_version), else: remote_version), 42 | } 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /lib/ex_wire/handshake/struct/auth_msg_v4.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake.Struct.AuthMsgV4 do 2 | @moduledoc """ 3 | Simple struct to wrap an auth msg. 4 | 5 | The RLPx v4 handshake auth is defined in EIP-8. 6 | """ 7 | 8 | alias ExthCrypto.ECIES.ECDH 9 | 10 | defstruct [ 11 | :signature, 12 | :remote_public_key, 13 | :remote_nonce, 14 | :remote_version, 15 | :remote_ephemeral_public_key 16 | ] 17 | 18 | @type t :: %__MODULE__{ 19 | signature: ExthCrypto.signature, 20 | remote_public_key: ExthCrypto.Key.public_key, 21 | remote_nonce: binary(), 22 | remote_version: integer(), 23 | remote_ephemeral_public_key: ExthCrypto.Key.public_key, 24 | } 25 | 26 | @spec serialize(t) :: ExRLP.t 27 | def serialize(auth_msg) do 28 | [ 29 | auth_msg.signature, 30 | auth_msg.remote_public_key |> ExthCrypto.Key.der_to_raw, 31 | auth_msg.remote_nonce, 32 | auth_msg.remote_version |> :binary.encode_unsigned 33 | ] 34 | end 35 | 36 | @spec deserialize(ExRLP.t) :: t 37 | def deserialize(rlp) do 38 | [ 39 | signature | 40 | [remote_public_key | 41 | [remote_nonce | 42 | [remote_version | 43 | _tl 44 | ]]]] = rlp 45 | 46 | %__MODULE__{ 47 | signature: signature, 48 | remote_public_key: remote_public_key |> ExthCrypto.Key.raw_to_der, 49 | remote_nonce: remote_nonce, 50 | remote_version: (if is_binary(remote_version), do: :binary.decode_unsigned(remote_version), else: remote_version), 51 | } 52 | end 53 | 54 | @doc """ 55 | Sets the remote ephemeral public key for a given auth msg, based on our secret 56 | and the keys passed from remote. 57 | 58 | # TODO: Test 59 | # TODO: Multiple possible values and no recovery key? 60 | """ 61 | @spec set_remote_ephemeral_public_key(t, ExthCrypto.Key.private_key) :: t 62 | def set_remote_ephemeral_public_key(auth_msg, my_static_private_key) do 63 | shared_secret = ECDH.generate_shared_secret(my_static_private_key, auth_msg.remote_public_key) 64 | shared_secret_xor_nonce = :crypto.exor(shared_secret, auth_msg.remote_nonce) 65 | 66 | {:ok, remote_ephemeral_public_key} = ExthCrypto.Signature.recover(shared_secret_xor_nonce, auth_msg.signature, 0) 67 | 68 | %{auth_msg | remote_ephemeral_public_key: remote_ephemeral_public_key} 69 | end 70 | 71 | end -------------------------------------------------------------------------------- /lib/ex_wire/message.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message do 2 | @moduledoc """ 3 | Defines a behavior for messages so that they can be 4 | easily encoded and decoded. 5 | """ 6 | 7 | defmodule UnknownMessageError do 8 | defexception [:message] 9 | end 10 | 11 | @type t :: module() 12 | @type message_id :: integer() 13 | 14 | @callback message_id() :: message_id 15 | @callback encode(t) :: binary() 16 | @callback to(t) :: ExWire.Endpoint.t | nil 17 | 18 | @message_types %{ 19 | 0x01 => ExWire.Message.Ping, 20 | 0x02 => ExWire.Message.Pong, 21 | 0x03 => ExWire.Message.FindNeighbours, 22 | 0x04 => ExWire.Message.Neighbours, 23 | } 24 | 25 | @doc """ 26 | Decodes a message of given `type` based on the encoded 27 | data. Effectively reverses the `decode/1` function. 28 | 29 | ## Examples 30 | 31 | iex> ExWire.Message.decode(0x01, <<210, 1, 199, 132, 1, 2, 3, 4, 128, 5, 199, 132, 5, 6, 7, 8, 6, 128, 4>>) 32 | %ExWire.Message.Ping{ 33 | version: 1, 34 | from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 35 | to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 36 | timestamp: 4 37 | } 38 | 39 | iex> ExWire.Message.decode(0x02, <<202, 199, 132, 5, 6, 7, 8, 6, 128, 2, 3>>) 40 | %ExWire.Message.Pong{ 41 | to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, hash: <<2>>, timestamp: 3 42 | } 43 | 44 | iex> ExWire.Message.decode(0x99, <<>>) 45 | ** (ExWire.Message.UnknownMessageError) Unknown message type: 0x99 46 | """ 47 | @spec decode(integer(), binary()) :: t 48 | def decode(type, data) do 49 | case @message_types[type] do 50 | nil -> raise UnknownMessageError, "Unknown message type: #{inspect type, base: :hex}" 51 | mod -> mod.decode(data) 52 | end 53 | end 54 | 55 | @doc """ 56 | Encoded a message by concatting its `message_id` to 57 | the encoded data of the message itself. 58 | 59 | ## Examples 60 | 61 | iex> ExWire.Message.encode( 62 | ...> %ExWire.Message.Ping{ 63 | ...> version: 1, 64 | ...> from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 65 | ...> to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 66 | ...> timestamp: 4 67 | ...> } 68 | ...> ) 69 | <<1, 214, 1, 201, 132, 1, 2, 3, 4, 128, 130, 0, 5, 201, 132, 5, 6, 7, 8, 130, 0, 6, 128, 4>> 70 | 71 | iex> ExWire.Message.encode(%ExWire.Message.Pong{to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, hash: <<2>>, timestamp: 3}) 72 | <<2, 204, 201, 132, 5, 6, 7, 8, 130, 0, 6, 128, 2, 3>> 73 | """ 74 | @spec encode(t) :: binary() 75 | def encode(message) do 76 | <> <> message.__struct__.encode(message) 77 | end 78 | 79 | end -------------------------------------------------------------------------------- /lib/ex_wire/message/find_neighbours.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.FindNeighbours do 2 | @moduledoc """ 3 | A wrapper for ExWire's `FindNeighbours` message. 4 | 5 | "Id of a node. The responding node will send back nodes closest to the target." 6 | """ 7 | 8 | @behaviour ExWire.Message 9 | @message_id 0x03 10 | 11 | defstruct [ 12 | target: nil, 13 | timestamp: nil, 14 | ] 15 | 16 | @type t :: %__MODULE__{ 17 | target: ExWire.node_id, 18 | timestamp: integer() 19 | } 20 | 21 | @spec message_id() :: ExWire.Message.message_id 22 | def message_id, do: @message_id 23 | 24 | @doc """ 25 | Decodes a given message binary, which is assumed 26 | to be an RLP encoded list of elements. 27 | 28 | ## Examples 29 | 30 | iex> ExWire.Message.FindNeighbours.decode([<<1>>, 2] |> ExRLP.encode) 31 | %ExWire.Message.FindNeighbours{ 32 | target: <<1>>, 33 | timestamp: 2, 34 | } 35 | 36 | iex> ExWire.Message.FindNeighbours.decode([<<1>>] |> ExRLP.encode) 37 | ** (MatchError) no match of right hand side value: [<<1>>] 38 | """ 39 | @spec decode(binary()) :: t 40 | def decode(data) do 41 | [target, timestamp] = ExRLP.decode(data) 42 | 43 | %__MODULE__{ 44 | target: target, 45 | timestamp: :binary.decode_unsigned(timestamp), 46 | } 47 | end 48 | 49 | @doc """ 50 | Given a FindNeighbours message, encodes it so it can be sent on the wire in RLPx. 51 | 52 | ## Examples 53 | 54 | iex> ExWire.Message.FindNeighbours.encode(%ExWire.Message.FindNeighbours{target: <<1>>, timestamp: 2}) 55 | ...> |> ExRLP.decode() 56 | [<<1>>, <<2>>] 57 | """ 58 | @spec encode(t) :: binary() 59 | def encode(%__MODULE__{target: target, timestamp: timestamp}) do 60 | ExRLP.encode([ 61 | target, 62 | timestamp, 63 | ]) 64 | end 65 | 66 | @doc """ 67 | FindNeighbours messages do not specify a destination. 68 | 69 | ## Examples 70 | 71 | iex> ExWire.Message.FindNeighbours.to(%ExWire.Message.FindNeighbours{target: <<1>>, timestamp: 2}) 72 | nil 73 | """ 74 | @spec to(t) :: Endpoint.t | nil 75 | def to(_message), do: nil 76 | 77 | end 78 | -------------------------------------------------------------------------------- /lib/ex_wire/message/neighbours.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.Neighbours do 2 | @moduledoc """ 3 | A wrapper for ExWire's `Neighbours` message. 4 | """ 5 | 6 | alias ExWire.Struct.Neighbour 7 | 8 | @behaviour ExWire.Message 9 | @message_id 0x04 10 | 11 | defstruct [ 12 | nodes: [], 13 | timestamp: [], 14 | ] 15 | 16 | @type t :: %__MODULE__{ 17 | nodes: [Neighbour.t], 18 | timestamp: integer() 19 | } 20 | 21 | @spec message_id() :: ExWire.Message.message_id 22 | def message_id, do: @message_id 23 | 24 | @doc """ 25 | Decodes a given message binary, which is assumed 26 | to be an RLP encoded list of elements. 27 | 28 | ## Examples 29 | 30 | iex> ExWire.Message.Neighbours.decode([ 31 | ...> [], 32 | ...> 2 33 | ...> ] |> ExRLP.encode) 34 | %ExWire.Message.Neighbours{ 35 | nodes: [], 36 | timestamp: 2, 37 | } 38 | 39 | iex> ExWire.Message.Neighbours.decode([ 40 | ...> [[<<1,2,3,4>>, <<>>, <<5>>, <<7, 7>>]], 41 | ...> 2 42 | ...> ] |> ExRLP.encode) 43 | %ExWire.Message.Neighbours{ 44 | nodes: [%ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, node: <<7, 7>>}], 45 | timestamp: 2, 46 | } 47 | 48 | iex> ExWire.Message.Neighbours.decode([ 49 | ...> [[<<1,2,3,4>>, <<>>, <<5>>, <<7, 7>>], [<<5,6,7,8>>, <<6>>, <<>>, <<8, 8>>]], 50 | ...> 2 51 | ...> ] |> ExRLP.encode) 52 | %ExWire.Message.Neighbours{ 53 | nodes: [ 54 | %ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, node: <<7, 7>>}, 55 | %ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, node: <<8, 8>>} 56 | ], 57 | timestamp: 2, 58 | } 59 | 60 | iex> ExWire.Message.Neighbours.decode([1] |> ExRLP.encode) 61 | ** (MatchError) no match of right hand side value: [<<1>>] 62 | """ 63 | @spec decode(binary()) :: t 64 | def decode(data) do 65 | [encoded_nodes, timestamp] = ExRLP.decode(data) 66 | 67 | %__MODULE__{ 68 | nodes: Enum.map(encoded_nodes, &Neighbour.decode/1), 69 | timestamp: :binary.decode_unsigned(timestamp), 70 | } 71 | end 72 | 73 | @doc """ 74 | Given a Neighbours message, encodes it so it can be sent on the wire in RLPx. 75 | 76 | ## Examples 77 | 78 | iex> ExWire.Message.Neighbours.encode(%ExWire.Message.Neighbours{nodes: [], timestamp: 1}) 79 | ...> |> ExRLP.decode() 80 | [[], <<1>>] 81 | 82 | iex> ExWire.Message.Neighbours.encode(%ExWire.Message.Neighbours{nodes: [ 83 | ...> %ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, node: <<7, 7>>}, 84 | ...> %ExWire.Struct.Neighbour{endpoint: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, node: <<8, 8>>}], 85 | ...> timestamp: 1}) 86 | ...> |> ExRLP.decode() 87 | [[[<<1,2,3,4>>, <<>>, <<0, 5>>, <<7, 7>>], [<<5,6,7,8>>, <<0, 6>>, <<>>, <<8, 8>>]], <<1>>] 88 | """ 89 | @spec encode(t) :: binary() 90 | def encode(%__MODULE__{nodes: nodes, timestamp: timestamp}) do 91 | ExRLP.encode([ 92 | Enum.map(nodes, &Neighbour.encode/1), 93 | timestamp, 94 | ]) 95 | end 96 | 97 | @doc """ 98 | Neighbours messages do not specify a destination. 99 | 100 | ## Examples 101 | 102 | iex> ExWire.Message.Neighbours.to(%ExWire.Message.Neighbours{nodes: [], timestamp: 1}) 103 | nil 104 | """ 105 | @spec to(t) :: Endpoint.t | nil 106 | def to(_message), do: nil 107 | 108 | end -------------------------------------------------------------------------------- /lib/ex_wire/message/ping.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.Ping do 2 | @moduledoc """ 3 | A wrapper for ExWire's `Ping` message. 4 | """ 5 | 6 | alias ExWire.Struct.Endpoint 7 | 8 | @behaviour ExWire.Message 9 | @message_id 0x01 10 | 11 | defstruct [ 12 | version: nil, 13 | from: nil, 14 | to: nil, 15 | timestamp: nil, 16 | ] 17 | 18 | @type t :: %__MODULE__{ 19 | version: integer(), 20 | from: Endpoint.t, 21 | to: Endpoint.t, 22 | timestamp: integer() 23 | } 24 | 25 | @spec message_id() :: ExWire.Message.message_id 26 | def message_id, do: @message_id 27 | 28 | @doc """ 29 | Decodes a given message binary, which is assumed 30 | to be an RLP encoded list of elements. 31 | 32 | ## Examples 33 | 34 | iex> ExWire.Message.Ping.decode([1, [<<1,2,3,4>>, <<>>, <<5>>], [<<5,6,7,8>>, <<6>>, <<>>], 4] |> ExRLP.encode) 35 | %ExWire.Message.Ping{ 36 | version: 1, 37 | from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 38 | to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 39 | timestamp: 4, 40 | } 41 | 42 | iex> ExWire.Message.Ping.decode([<<1>>] |> ExRLP.encode) 43 | ** (MatchError) no match of right hand side value: [<<1>>] 44 | """ 45 | @spec decode(binary()) :: t 46 | def decode(data) do 47 | [version, from, to, timestamp] = ExRLP.decode(data) 48 | 49 | %__MODULE__{ 50 | version: :binary.decode_unsigned(version), 51 | from: Endpoint.decode(from), 52 | to: Endpoint.decode(to), 53 | timestamp: :binary.decode_unsigned(timestamp), 54 | } 55 | end 56 | 57 | @doc """ 58 | Given a Ping message, encodes it so it can be sent on the wire in RLPx. 59 | 60 | ## Examples 61 | 62 | iex> ExWire.Message.Ping.encode(%ExWire.Message.Ping{ 63 | ...> version: 1, 64 | ...> from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 65 | ...> to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 66 | ...> timestamp: 4} 67 | ...> ) |> ExRLP.decode() 68 | [<<1>>, [<<1, 2, 3, 4>>, "", <<0, 5>>], [<<5, 6, 7, 8>>, <<0, 6>>, ""], <<4>>] 69 | """ 70 | @spec encode(t) :: binary() 71 | def encode(%__MODULE__{version: version, from: from, to: to, timestamp: timestamp}) do 72 | ExRLP.encode([ 73 | version, 74 | Endpoint.encode(from), 75 | Endpoint.encode(to), 76 | timestamp, 77 | ]) 78 | end 79 | 80 | @doc """ 81 | Ping messages specify a destination. 82 | 83 | ## Examples 84 | 85 | iex> ExWire.Message.Ping.to(%ExWire.Message.Ping{ 86 | ...> version: 1, 87 | ...> from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 88 | ...> to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 89 | ...> timestamp: 4} 90 | ...> ) 91 | %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6} 92 | """ 93 | @spec to(t) :: Endpoint.t | nil 94 | def to(message) do 95 | message.to 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/ex_wire/message/pong.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.Pong do 2 | @moduledoc """ 3 | A wrapper for ExWire's `Pong` message. 4 | """ 5 | 6 | alias ExWire.Struct.Endpoint 7 | 8 | @message_id 0x02 9 | 10 | defstruct [ 11 | to: nil, 12 | hash: nil, 13 | timestamp: nil, 14 | ] 15 | 16 | @type t :: %__MODULE__{ 17 | to: Endpoint.t, 18 | hash: binary(), 19 | timestamp: integer() 20 | } 21 | 22 | @spec message_id() :: ExWire.Message.message_id 23 | def message_id, do: @message_id 24 | 25 | @doc """ 26 | Decodes a given message binary, which is assumed 27 | to be an RLP encoded list of elements. 28 | 29 | ## Examples 30 | 31 | iex> ExWire.Message.Pong.decode([[<<1,2,3,4>>, <<>>, <<0, 5>>], <<2>>, 3] |> ExRLP.encode) 32 | %ExWire.Message.Pong{ 33 | to: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 34 | hash: <<2>>, 35 | timestamp: 3, 36 | } 37 | 38 | iex> ExWire.Message.Pong.decode([<<1>>] |> ExRLP.encode) 39 | ** (MatchError) no match of right hand side value: [<<1>>] 40 | """ 41 | @spec decode(binary()) :: t 42 | def decode(data) do 43 | [to, hash, timestamp] = ExRLP.decode(data) 44 | 45 | %__MODULE__{ 46 | to: Endpoint.decode(to), 47 | hash: hash, 48 | timestamp: :binary.decode_unsigned(timestamp), 49 | } 50 | end 51 | 52 | @doc """ 53 | Given a Pong message, encodes it so it can be sent on the wire in RLPx. 54 | 55 | ## Examples 56 | 57 | iex> ExWire.Message.Pong.encode(%ExWire.Message.Pong{ 58 | ...> to: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 59 | ...> hash: <<2>>, 60 | ...> timestamp: 3} 61 | ...> ) |> ExRLP.decode() 62 | [[<<1, 2, 3, 4>>, "", <<0, 5>>], <<2>>, <<3>>] 63 | """ 64 | @spec encode(t) :: binary() 65 | def encode(%__MODULE__{to: to, hash: hash, timestamp: timestamp}) do 66 | ExRLP.encode([ 67 | Endpoint.encode(to), 68 | hash, 69 | timestamp, 70 | ]) 71 | end 72 | 73 | @doc """ 74 | Pong messages should be routed to given endpoint. 75 | 76 | ## Examples 77 | 78 | iex> ExWire.Message.Pong.to(%ExWire.Message.Pong{ 79 | ...> to: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 80 | ...> hash: <<2>>, 81 | ...> timestamp: 3} 82 | ...> ) 83 | %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil} 84 | """ 85 | @spec to(t) :: Endpoint.t | nil 86 | def to(message) do 87 | message.to 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /lib/ex_wire/network.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Network do 2 | @moduledoc """ 3 | This module will handle the business logic for processing 4 | incoming messages from the network. We will, for instance, 5 | decide to respond pong to any incoming ping. 6 | """ 7 | 8 | require Logger 9 | 10 | alias ExWire.Crypto 11 | alias ExWire.Handler 12 | alias ExWire.Protocol 13 | alias ExWire.Struct.Endpoint 14 | 15 | defmodule InboundMessage do 16 | @moduledoc """ 17 | Struct to define an inbound message from a remote peer 18 | """ 19 | 20 | defstruct [ 21 | data: nil, 22 | server_pid: nil, 23 | remote_host: nil, 24 | timestamp: nil 25 | ] 26 | 27 | @type t :: %__MODULE__{ 28 | data: binary(), 29 | server_pid: pid(), 30 | remote_host: ExWire.Struct.Endpoint.t, 31 | timestamp: integer() 32 | } 33 | end 34 | 35 | @type handler_action :: :no_action | {:sent_message, atom()} 36 | 37 | @doc """ 38 | Top-level receiver function to process an incoming message. 39 | We'll first validate the message, and then pass it to 40 | the appropriate handler. 41 | 42 | ## Examples 43 | 44 | iex> ping_data = [1, [<<1,2,3,4>>, <<>>, <<5>>], [<<5,6,7,8>>, <<6>>, <<>>], 4] |> ExRLP.encode 45 | iex> payload = <<0x01::8>> <> ping_data 46 | iex> payload_hash = ExWire.Crypto.hash(payload) 47 | iex> {signature, _r, _s, recovery_bit} = ExthCrypto.Signature.sign_digest(payload_hash, ExthCrypto.Test.private_key) 48 | iex> total_payload = signature <> <> <> payload 49 | iex> hash = ExWire.Crypto.hash(total_payload) 50 | iex> ExWire.Network.receive(%ExWire.Network.InboundMessage{ 51 | ...> data: hash <> total_payload, 52 | ...> server_pid: self(), 53 | ...> remote_host: nil, 54 | ...> timestamp: 123, 55 | ...> }, nil) 56 | {:sent_message, ExWire.Message.Pong} 57 | 58 | iex> ping_data = [1, [<<1,2,3,4>>, <<>>, <<5>>], [<<5,6,7,8>>, <<6>>, <<>>], 4] |> ExRLP.encode 59 | iex> payload = <<0x01::8>> <> ping_data 60 | iex> payload_hash = ExWire.Crypto.hash(payload) 61 | iex> {signature, _r, _s, recovery_bit} = ExthCrypto.Signature.sign_digest(payload_hash, ExthCrypto.Test.private_key) 62 | iex> total_payload = signature <> <> <> payload 63 | iex> hash = ExWire.Crypto.hash("hello") 64 | iex> ExWire.Network.receive(%ExWire.Network.InboundMessage{ 65 | ...> data: hash <> total_payload, 66 | ...> server_pid: self(), 67 | ...> remote_host: nil, 68 | ...> timestamp: 123, 69 | ...> }, nil) 70 | ** (ExWire.Crypto.HashMismatch) Invalid hash 71 | """ 72 | @spec receive(InboundMessage.t, identifier() | nil) :: handler_action 73 | def receive(inbound_message=%InboundMessage{data: data, server_pid: server_pid}, discovery_pid) do 74 | :ok = assert_integrity(data) 75 | 76 | inbound_message 77 | |> get_params 78 | |> dispatch_handler(server_pid, discovery_pid) 79 | end 80 | 81 | @doc """ 82 | Given the data of an inbound message, we'll run a quick SHA3 sum to verify 83 | the integrity of the message. 84 | 85 | ## Examples 86 | 87 | iex> ExWire.Network.assert_integrity(ExWire.Crypto.hash("hi mom") <> "hi mom") 88 | :ok 89 | 90 | iex> ExWire.Network.assert_integrity(<<1::256>> <> "hi mom") 91 | ** (ExWire.Crypto.HashMismatch) Invalid hash 92 | """ 93 | @spec assert_integrity(binary()) :: :ok 94 | def assert_integrity(<< hash :: size(256), payload :: bits >>) do 95 | Crypto.assert_hash(payload, <>) 96 | end 97 | 98 | @doc """ 99 | Returns a Handler Params for the given inbound message. 100 | 101 | ## Examples 102 | 103 | iex> ping_data = [1, [<<1,2,3,4>>, <<>>, <<5>>], [<<5,6,7,8>>, <<6>>, <<>>], 4] |> ExRLP.encode 104 | iex> payload = <<0x01::8>> <> ping_data 105 | iex> hash = ExWire.Crypto.hash(payload) 106 | iex> {signature, _r, _s, recovery_bit} = ExthCrypto.Signature.sign_digest(hash, ExthCrypto.Test.private_key) 107 | iex> params = ExWire.Network.get_params(%ExWire.Network.InboundMessage{ 108 | ...> data: hash <> signature <> <> <> payload, 109 | ...> server_pid: self(), 110 | ...> remote_host: nil, 111 | ...> timestamp: 5, 112 | ...> }) 113 | iex> params.hash 114 | <<162, 185, 143, 17, 69, 224, 221, 60, 169, 194, 154, 173, 122, 242, 156, 30, 197, 44, 131, 3, 210, 37, 73, 157, 104, 180, 128, 48, 106, 42, 163, 213>> 115 | iex> params.node_id 116 | <<54, 241, 224, 126, 85, 135, 69, 213, 129, 115, 3, 41, 161, 217, 87, 215, 159, 64, 17, 167, 128, 113, 172, 232, 46, 34, 145, 136, 72, 160, 207, 161, 171, 255, 26, 163, 160, 158, 227, 196, 92, 62, 119, 84, 156, 99, 224, 155, 120, 250, 153, 134, 180, 218, 177, 186, 200, 199, 106, 97, 103, 50, 215, 114>> 117 | iex> params.type 118 | 1 119 | 120 | iex> payload_data = [] |> ExRLP.encode 121 | iex> payload = <<0xff::8>> <> payload_data 122 | iex> hash = ExWire.Crypto.hash(payload) 123 | iex> {signature, _r, _s, recovery_bit} = ExthCrypto.Signature.sign_digest(hash, ExthCrypto.Test.private_key) 124 | iex> params = ExWire.Network.get_params(%ExWire.Network.InboundMessage{ 125 | ...> data: hash <> signature <> <> <> payload, 126 | ...> server_pid: self(), 127 | ...> remote_host: nil, 128 | ...> timestamp: 5, 129 | ...> }) 130 | iex> params.hash 131 | <<19, 42, 97, 10, 60, 19, 10, 67, 247, 221, 97, 93, 120, 59, 83, 60, 207, 199, 47, 217, 115, 186, 202, 251, 110, 61, 69, 88, 179, 115, 85, 52>> 132 | iex> params.node_id 133 | <<54, 241, 224, 126, 85, 135, 69, 213, 129, 115, 3, 41, 161, 217, 87, 215, 159, 64, 17, 167, 128, 113, 172, 232, 46, 34, 145, 136, 72, 160, 207, 161, 171, 255, 26, 163, 160, 158, 227, 196, 92, 62, 119, 84, 156, 99, 224, 155, 120, 250, 153, 134, 180, 218, 177, 186, 200, 199, 106, 97, 103, 50, 215, 114>> 134 | iex> params.type 135 | 255 136 | """ 137 | @spec get_params(InboundMessage.t) :: Handler.Params.t 138 | def get_params(%InboundMessage{ 139 | data: << 140 | hash :: binary-size(32), 141 | signature :: binary-size(64), 142 | recovery_id:: integer-size(8), 143 | type:: binary-size(1), 144 | data :: bitstring 145 | >>, 146 | remote_host: remote_host, 147 | timestamp: timestamp 148 | }) do 149 | # Recover public key 150 | {:ok, node_id} = ExthCrypto.Signature.recover(Crypto.hash(type <> data), signature, recovery_id) 151 | 152 | %Handler.Params{ 153 | remote_host: remote_host, 154 | signature: signature, 155 | recovery_id: recovery_id, 156 | hash: hash, 157 | type: type |> :binary.decode_unsigned, 158 | data: data, 159 | timestamp: timestamp, 160 | node_id: node_id |> ExthCrypto.Key.der_to_raw 161 | } 162 | end 163 | 164 | @doc """ 165 | Function to pass message to the appropriate handler. E.g. for a ping 166 | we'll pass the decoded message to `ExWire.Handlers.Ping.handle/1`. 167 | 168 | ## Examples 169 | 170 | iex> %ExWire.Handler.Params{ 171 | ...> data: <<210, 1, 199, 132, 1, 2, 3, 4, 128, 5, 199, 132, 5, 6, 7, 8, 6, 128, 4>>, 172 | ...> hash: <<162, 185, 143, 17, 69, 224, 221, 60, 169, 194, 154, 173, 122, 242, 156, 30, 197, 44, 131, 3, 210, 37, 73, 157, 104, 180, 128, 48, 106, 42, 163, 213>>, 173 | ...> node_id: <<54, 241, 224, 126, 85, 135, 69, 213, 129, 115, 3, 41, 161, 217, 87, 215, 159, 64, 17, 167, 128, 113, 172, 232, 46, 34, 145, 136, 72, 160, 207, 161, 171, 255, 26, 163, 160, 158, 227, 196, 92, 62, 119, 84, 156, 99, 224, 155, 120, 250, 153, 134, 180, 218, 177, 186, 200, 199, 106, 97, 103, 50, 215, 114>>, 174 | ...> recovery_id: 0, 175 | ...> remote_host: nil, 176 | ...> signature: <<65, 254, 238, 89, 78, 155, 122, 241, 232, 167, 109, 23, 160, 87, 80, 15, 4, 162, 38, 254, 96, 108, 107, 103, 41, 254, 149, 181, 176, 63, 188, 145, 19, 151, 239, 5, 242, 146, 95, 207, 102, 142, 200, 154, 88, 213, 37, 177, 174, 107, 73, 132, 0, 116, 186, 24, 105, 167, 134, 131, 86, 196, 183, 236>>, 177 | ...> timestamp: 5, 178 | ...> type: 1 179 | ...> } 180 | ...> |> ExWire.Network.dispatch_handler(nil, nil) 181 | {:sent_message, ExWire.Message.Pong} 182 | 183 | iex> %ExWire.Handler.Params{ 184 | ...> data: <<192>>, 185 | ...> hash: <<19, 42, 97, 10, 60, 19, 10, 67, 247, 221, 97, 93, 120, 59, 83, 60, 207, 199, 47, 217, 115, 186, 202, 251, 110, 61, 69, 88, 179, 115, 85, 52>>, 186 | ...> node_id: <<54, 241, 224, 126, 85, 135, 69, 213, 129, 115, 3, 41, 161, 217, 87, 215, 159, 64, 17, 167, 128, 113, 172, 232, 46, 34, 145, 136, 72, 160, 207, 161, 171, 255, 26, 163, 160, 158, 227, 196, 92, 62, 119, 84, 156, 99, 224, 155, 120, 250, 153, 134, 180, 218, 177, 186, 200, 199, 106, 97, 103, 50, 215, 114>>, 187 | ...> recovery_id: 0, 188 | ...> remote_host: nil, 189 | ...> signature: <<43, 58, 139, 136, 136, 159, 177, 17, 78, 215, 13, 0, 38, 221, 76, 104, 44, 24, 110, 130, 189, 97, 92, 82, 144, 50, 236, 190, 10, 49, 145, 134, 123, 7, 213, 199, 143, 106, 226, 104, 114, 135, 135, 124, 133, 247, 81, 71, 76, 235, 255, 145, 87, 54, 232, 94, 245, 245, 219, 21, 180, 174, 254, 15>>, 190 | ...> timestamp: 5, 191 | ...> type: 255 192 | ...> } 193 | ...> |> ExWire.Network.dispatch_handler(nil, nil) 194 | :no_action 195 | """ 196 | @spec dispatch_handler(Handler.Params.t, identifier(), identifier() | nil) :: handler_action 197 | def dispatch_handler(params, server_pid, discovery_pid) do 198 | case Handler.dispatch(params.type, params, discovery_pid) do 199 | :not_implemented -> :no_action 200 | :no_response -> :no_action 201 | {:respond, response_message} -> 202 | # TODO: This is a simple way to determine who to send the message to, 203 | # but we may want to revise. 204 | to = response_message.__struct__.to(response_message) || params.remote_host 205 | 206 | send(response_message, server_pid, to) 207 | end 208 | end 209 | 210 | @doc """ 211 | Sends a message asynchronously via casting a message 212 | to our running `gen_server`. 213 | 214 | ## Examples 215 | 216 | iex> message = %ExWire.Message.Pong{ 217 | ...> to: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 218 | ...> hash: <<2>>, 219 | ...> timestamp: 3, 220 | ...> } 221 | iex> ExWire.Network.send(message, self(), %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: 5}) 222 | {:sent_message, ExWire.Message.Pong} 223 | iex> receive do m -> m end 224 | {:"$gen_cast", 225 | {:send, 226 | %{ 227 | data: ExWire.Protocol.encode(message, ExWire.Config.private_key()), 228 | to: %ExWire.Struct.Endpoint{ 229 | ip: {1, 2, 3, 4}, 230 | tcp_port: nil, 231 | udp_port: 5} 232 | } 233 | } 234 | } 235 | """ 236 | @spec send(ExWire.Message.t, identifier(), ExWire.Struct.Endpoint.t) :: handler_action 237 | def send(message, server_pid, to) do 238 | Logger.debug("[Network] Sending #{to_string(message.__struct__)} message to #{to.ip |> Endpoint.ip_to_string}") 239 | 240 | GenServer.cast( 241 | server_pid, 242 | { 243 | :send, 244 | %{ 245 | to: to, 246 | data: Protocol.encode(message, ExWire.Config.private_key()), 247 | } 248 | } 249 | ) 250 | 251 | {:sent_message, message.__struct__} 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /lib/ex_wire/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet do 2 | @moduledoc """ 3 | Packets handle serializing and deserializing framed packet data from 4 | the DevP2P and Eth Wire Protocols. They also handle how to respond 5 | to incoming packets. 6 | """ 7 | 8 | alias ExWire.Packet 9 | 10 | @type packet :: %{} 11 | @type block_identifier :: binary() | integer() 12 | @type block_hash :: {binary(), integer()} 13 | 14 | @callback serialize(packet) :: ExRLP.t 15 | @callback deserialize(ExRLP.t) :: packet 16 | 17 | @type handle_response :: :ok | :activate | :peer_disconnect | {:disconnect, atom()} | {:send, struct()} 18 | @callback handle(packet) :: handle_response 19 | 20 | @packet_types %{ 21 | 0x00 => Packet.Hello, 22 | 0x01 => Packet.Disconnect, 23 | 0x02 => Packet.Ping, 24 | 0x03 => Packet.Pong, 25 | ### Ethereum Sub-protocol 26 | 0x10 => Packet.Status, 27 | 0x11 => Packet.NewBlockHashes, # New model syncing (PV62) 28 | 0x12 => Packet.Transactions, 29 | 0x13 => Packet.GetBlockHeaders, # New model syncing (PV62) 30 | 0x14 => Packet.BlockHeaders, # New model syncing (PV62) 31 | 0x15 => Packet.GetBlockBodies, # New model syncing (PV62) 32 | 0x16 => Packet.BlockBodies, # New model syncing (PV62) 33 | 0x17 => Packet.NewBlock, 34 | ### Fast synchronization (PV63) 35 | # 0x1d => Packet.GetNodeData, 36 | # 0x1e => Packet.NodeData, 37 | # 0x1f => Packet.GetReceipts, 38 | # 0x20 => Packet.Receipts 39 | } 40 | 41 | @packet_types_inverted (for {k, v} <- @packet_types, do: {v, k}) |> Enum.into(%{}) 42 | 43 | @doc """ 44 | Returns the module which contains functions to 45 | `serialize/1`, `deserialize/1` and `handle/1` the given `packet_type`. 46 | 47 | ## Examples 48 | 49 | iex> ExWire.Packet.get_packet_mod(0x00) 50 | {:ok, ExWire.Packet.Hello} 51 | 52 | iex> ExWire.Packet.get_packet_mod(0x10) 53 | {:ok, ExWire.Packet.Status} 54 | 55 | iex> ExWire.Packet.get_packet_mod(0xFF) 56 | :unknown_packet_type 57 | """ 58 | @spec get_packet_mod(integer()) :: {:ok, module()} | :unknown_packet_type 59 | def get_packet_mod(packet_type) do 60 | case @packet_types[packet_type] do 61 | nil -> :unknown_packet_type 62 | packet_type -> {:ok, packet_type} 63 | end 64 | end 65 | 66 | @doc """ 67 | Returns the eth id of the given packet based on the struct. 68 | 69 | ## Examples 70 | 71 | iex> ExWire.Packet.get_packet_type(%ExWire.Packet.Hello{}) 72 | {:ok, 0x00} 73 | 74 | iex> ExWire.Packet.get_packet_type(%ExWire.Packet.Status{}) 75 | {:ok, 0x10} 76 | 77 | iex> ExWire.Packet.get_packet_type(%ExWire.Struct.Neighbour{}) 78 | :unknown_packet 79 | """ 80 | @spec get_packet_type(struct()) :: {:ok, integer()} | :unknown_packet 81 | def get_packet_type(_packet=%{__struct__: packet_struct}) do 82 | case @packet_types_inverted[packet_struct] do 83 | nil -> :unknown_packet 84 | packet_type -> {:ok, packet_type} 85 | end 86 | end 87 | 88 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/block_bodies.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.BlockBodies do 2 | @moduledoc """ 3 | Eth Wire Packet for getting block bodies from a peer. 4 | 5 | ``` 6 | **BlockBodies** [`+0x06`, [`transactions_0`, `uncles_0`] , ...] 7 | 8 | Reply to `GetBlockBodies`. The items in the list (following the message ID) are 9 | some of the blocks, minus the header, in the format described in the main Ethereum 10 | specification, previously asked for in a `GetBlockBodies` message. This may 11 | validly contain no items if no blocks were able to be returned for the 12 | `GetBlockBodies` query. 13 | ``` 14 | """ 15 | 16 | require Logger 17 | 18 | alias ExWire.Struct.Block 19 | 20 | @behaviour ExWire.Packet 21 | 22 | @type t :: %__MODULE__{ 23 | blocks: [Block.t] 24 | } 25 | 26 | defstruct [ 27 | :blocks 28 | ] 29 | 30 | @doc """ 31 | Given a BlockBodies packet, serializes for transport over Eth Wire Protocol. 32 | 33 | ## Examples 34 | 35 | iex> %ExWire.Packet.BlockBodies{ 36 | ...> blocks: [ 37 | ...> %ExWire.Struct.Block{transactions_list: [[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], ommers: [<<1::256>>]}, 38 | ...> %ExWire.Struct.Block{transactions_list: [[<<6>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], ommers: [<<1::256>>]} 39 | ...> ] 40 | ...> } 41 | ...> |> ExWire.Packet.BlockBodies.serialize() 42 | [ [[[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], [<<1::256>>]], [[[<<6>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], [<<1::256>>]] ] 43 | """ 44 | @spec serialize(t) :: ExRLP.t 45 | def serialize(packet=%__MODULE__{}) do 46 | for block <- packet.blocks, do: Block.serialize(block) 47 | end 48 | 49 | @doc """ 50 | Given an RLP-encoded BlockBodies packet from Eth Wire Protocol, 51 | decodes into a BlockBodies struct. 52 | 53 | ## Examples 54 | 55 | iex> ExWire.Packet.BlockBodies.deserialize([ [[[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], [<<1::256>>]], [[[<<6>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], [<<1::256>>]] ]) 56 | %ExWire.Packet.BlockBodies{ 57 | blocks: [ 58 | %ExWire.Struct.Block{ 59 | transactions_list: [[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], 60 | transactions: [%Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1::160>>, value: 8, v: 27, r: 9, s: 10, data: "hi"}], 61 | ommers: [<<1::256>>] 62 | }, 63 | %ExWire.Struct.Block{ 64 | transactions_list: [[<<6>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], 65 | transactions: [%Blockchain.Transaction{nonce: 6, gas_price: 6, gas_limit: 7, to: <<1::160>>, value: 8, v: 27, r: 9, s: 10, data: "hi"}], 66 | ommers: [<<1::256>>] 67 | } 68 | ] 69 | } 70 | """ 71 | @spec deserialize(ExRLP.t) :: t 72 | def deserialize(rlp) do 73 | blocks = for block <- rlp, do: Block.deserialize(block) 74 | 75 | %__MODULE__{ 76 | blocks: blocks 77 | } 78 | end 79 | 80 | @doc """ 81 | Handles a BlockBodies message. This is when we have received 82 | a given set of blocks back from a peer. 83 | 84 | ## Examples 85 | 86 | iex> %ExWire.Packet.GetBlockBodies{hashes: [<<5>>, <<6>>]} 87 | ...> |> ExWire.Packet.GetBlockBodies.handle() 88 | :ok 89 | """ 90 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 91 | def handle(packet=%__MODULE__{}) do 92 | Logger.info("[Packet] Peer sent #{Enum.count(packet.blocks)} block(s).") 93 | 94 | :ok 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /lib/ex_wire/packet/block_headers.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.BlockHeaders do 2 | @moduledoc """ 3 | Eth Wire Packet for getting block headers from a peer. 4 | 5 | ``` 6 | **BlockHeaders** [`+0x04`, `blockHeader_0`, `blockHeader_1`, ...] 7 | 8 | Reply to `GetBlockHeaders`. The items in the list (following the message ID) are 9 | block headers in the format described in the main Ethereum specification, previously 10 | asked for in a `GetBlockHeaders` message. This may validly contain no block headers 11 | if no block headers were able to be returned for the `GetBlockHeaders` query. 12 | ``` 13 | """ 14 | 15 | require Logger 16 | 17 | @behaviour ExWire.Packet 18 | 19 | @type t :: %__MODULE__{ 20 | headers: [Block.Header.t] 21 | } 22 | 23 | defstruct [ 24 | :headers 25 | ] 26 | 27 | @doc """ 28 | Given a BlockHeaders packet, serializes for transport over Eth Wire Protocol. 29 | 30 | ## Examples 31 | 32 | iex> %ExWire.Packet.BlockHeaders{ 33 | ...> headers: [ 34 | ...> %Block.Header{parent_hash: <<1::256>>, ommers_hash: <<2::256>>, beneficiary: <<3::160>>, state_root: <<4::256>>, transactions_root: <<5::256>>, receipts_root: <<6::256>>, logs_bloom: <<>>, difficulty: 5, number: 1, gas_limit: 5, gas_used: 3, timestamp: 6, extra_data: "Hi mom", mix_hash: <<7::256>>, nonce: <<8::64>>} 35 | ...> ] 36 | ...> } 37 | ...> |> ExWire.Packet.BlockHeaders.serialize 38 | [ [<<1::256>>, <<2::256>>, <<3::160>>, <<4::256>>, <<5::256>>, <<6::256>>, <<>>, 5, 1, 5, 3, 6, "Hi mom", <<7::256>>, <<8::64>>] ] 39 | """ 40 | @spec serialize(t) :: ExRLP.t 41 | def serialize(packet=%__MODULE__{}) do 42 | for header <- packet.headers, do: Block.Header.serialize(header) 43 | end 44 | 45 | @doc """ 46 | Given an RLP-encoded BlockBodies packet from Eth Wire Protocol, 47 | decodes into a BlockBodies struct. 48 | 49 | ## Examples 50 | 51 | iex> ExWire.Packet.BlockHeaders.deserialize([ [<<1::256>>, <<2::256>>, <<3::160>>, <<4::256>>, <<5::256>>, <<6::256>>, <<>>, <<5>>, <<1>>, <<5>>, <<3>>, <<6>>, "Hi mom", <<7::256>>, <<8::64>>] ]) 52 | %ExWire.Packet.BlockHeaders{ 53 | headers: [ 54 | %Block.Header{parent_hash: <<1::256>>, ommers_hash: <<2::256>>, beneficiary: <<3::160>>, state_root: <<4::256>>, transactions_root: <<5::256>>, receipts_root: <<6::256>>, logs_bloom: <<>>, difficulty: 5, number: 1, gas_limit: 5, gas_used: 3, timestamp: 6, extra_data: "Hi mom", mix_hash: <<7::256>>, nonce: <<8::64>>}, 55 | ] 56 | } 57 | """ 58 | @spec deserialize(ExRLP.t) :: t 59 | def deserialize(rlp) do 60 | headers = for header <- rlp, do: Block.Header.deserialize(header) 61 | 62 | %__MODULE__{ 63 | headers: headers 64 | } 65 | end 66 | 67 | @doc """ 68 | Handles a BlockHeaders message. This is when we have received 69 | a given set of block headers back from a peer. 70 | 71 | ## Examples 72 | 73 | iex> %ExWire.Packet.BlockHeaders{headers: [ %Block.Header{parent_hash: <<1::256>>, ommers_hash: <<2::256>>, beneficiary: <<3::160>>, state_root: <<4::256>>, transactions_root: <<5::256>>, receipts_root: <<6::256>>, logs_bloom: <<>>, difficulty: 5, number: 1, gas_limit: 5, gas_used: 3, timestamp: 6, extra_data: "Hi mom", mix_hash: <<7::256>>, nonce: <<8::64>>} ]} 74 | ...> |> ExWire.Packet.BlockHeaders.handle() 75 | :ok 76 | """ 77 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 78 | def handle(packet=%__MODULE__{}) do 79 | # TODO: Do. 80 | Logger.debug("[Packet] Peer sent #{Enum.count(packet.headers)} header(s)") 81 | 82 | # packet.headers |> Exth.inspect("Got headers, requesting more?") 83 | 84 | :ok 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/ex_wire/packet/disconnect.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.Disconnect do 2 | @moduledoc """ 3 | Disconnect is when a peer wants to end a connection for a reason. 4 | 5 | ``` 6 | **Disconnect** `0x01` [`reason`: `P`] 7 | 8 | Inform the peer that a disconnection is imminent; if received, a peer should 9 | disconnect immediately. When sending, well-behaved hosts give their peers a 10 | fighting chance (read: wait 2 seconds) to disconnect to before disconnecting 11 | themselves. 12 | 13 | * `reason` is an optional integer specifying one of a number of reasons for disconnect: 14 | * `0x00` Disconnect requested; 15 | * `0x01` TCP sub-system error; 16 | * `0x02` Breach of protocol, e.g. a malformed message, bad RLP, incorrect magic number &c.; 17 | * `0x03` Useless peer; 18 | * `0x04` Too many peers; 19 | * `0x05` Already connected; 20 | * `0x06` Incompatible P2P protocol version; 21 | * `0x07` Null node identity received - this is automatically invalid; 22 | * `0x08` Client quitting; 23 | * `0x09` Unexpected identity (i.e. a different identity to a previous connection/what a trusted peer told us). 24 | * `0x0a` Identity is the same as this node (i.e. connected to itself); 25 | * `0x0b` Timeout on receiving a message (i.e. nothing received since sending last ping); 26 | * `0x10` Some other reason specific to a subprotocol. 27 | ``` 28 | """ 29 | 30 | require Logger 31 | 32 | @behaviour ExWire.Packet 33 | 34 | @type t :: %__MODULE__{ 35 | reason: integer() 36 | } 37 | 38 | defstruct [ 39 | :reason 40 | ] 41 | 42 | @reason_msgs %{ 43 | disconnect_request: "disconnect requested", 44 | tcp_sub_system_error: "TCP sub-system error", 45 | break_of_protocol: "breach of protocol", 46 | useless_peer: "useless peer", 47 | too_many_peers: "too many peers", 48 | already_connected: "already connected", 49 | incompatible_p2p_protcol_version: "incompatible P2P protocol version", 50 | null_node_identity_received: "null node identity received", 51 | client_quitting: "client quitting", 52 | unexpected_identity: "unexpected identity", 53 | identity_is_same_as_self: "identity is the same as this node", 54 | timeout_on_receiving_message: "timeout on receiving a message", 55 | other_reason: "some other reason specific to a subprotocol" 56 | } 57 | 58 | @reasons %{ 59 | disconnect_request: 0x00, 60 | tcp_sub_system_error: 0x01, 61 | break_of_protocol: 0x02, 62 | useless_peer: 0x03, 63 | too_many_peers: 0x04, 64 | already_connected: 0x05, 65 | incompatible_p2p_protcol_version: 0x06, 66 | null_node_identity_received: 0x07, 67 | client_quitting: 0x08, 68 | unexpected_identity: 0x09, 69 | identity_is_same_as_self: 0x0a, 70 | timeout_on_receiving_message: 0x0b, 71 | other_reason: 0x10, 72 | } 73 | 74 | @reasons_inverted (for {k, v} <- @reasons, do: {v, k}) |> Enum.into(%{}) 75 | 76 | @doc """ 77 | Given a Disconnect packet, serializes for transport over Eth Wire Protocol. 78 | 79 | ## Examples 80 | 81 | iex> %ExWire.Packet.Disconnect{reason: :timeout_on_receiving_message} 82 | ...> |> ExWire.Packet.Disconnect.serialize 83 | [0x0b] 84 | """ 85 | @spec serialize(t) :: ExRLP.t 86 | def serialize(packet=%__MODULE__{}) do 87 | [ 88 | Map.get(@reasons, packet.reason) 89 | ] 90 | end 91 | 92 | @doc """ 93 | Given an RLP-encoded Disconnect packet from Eth Wire Protocol, 94 | decodes into a Disconnect struct. 95 | 96 | ## Examples 97 | 98 | iex> ExWire.Packet.Disconnect.deserialize([<<0x0b>>]) 99 | %ExWire.Packet.Disconnect{reason: :timeout_on_receiving_message} 100 | """ 101 | @spec deserialize(ExRLP.t) :: t 102 | def deserialize(rlp) do 103 | [ 104 | reason 105 | ] = rlp 106 | 107 | %__MODULE__{ 108 | reason: @reasons_inverted[reason |> :binary.decode_unsigned] 109 | } 110 | end 111 | 112 | @doc """ 113 | Creates a new disconnect message with given reason. This 114 | function raises if `reason` is not a known reason. 115 | 116 | ## Examples 117 | 118 | iex> ExWire.Packet.Disconnect.new(:too_many_peers) 119 | %ExWire.Packet.Disconnect{reason: :too_many_peers} 120 | 121 | iex> ExWire.Packet.Disconnect.new(:something_else) 122 | ** (RuntimeError) Invalid reason 123 | """ 124 | def new(reason) do 125 | if @reasons[reason] == nil, do: raise "Invalid reason" 126 | 127 | %__MODULE__{ 128 | reason: reason 129 | } 130 | end 131 | 132 | @doc """ 133 | Returns a string interpretation of a reason for disconnect. 134 | 135 | ## Examples 136 | 137 | iex> ExWire.Packet.Disconnect.get_reason_msg(:timeout_on_receiving_message) 138 | "timeout on receiving a message" 139 | """ 140 | @spec get_reason_msg(integer()) :: String.t 141 | def get_reason_msg(reason) do 142 | @reason_msgs[reason] 143 | end 144 | 145 | @doc """ 146 | Handles a Disconnect message. We are instructed to disconnect, which 147 | we'll abide by. 148 | 149 | ## Examples 150 | 151 | iex> %ExWire.Packet.GetBlockBodies{hashes: [<<5>>, <<6>>]} 152 | ...> |> ExWire.Packet.GetBlockBodies.handle() 153 | :ok 154 | """ 155 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 156 | def handle(packet=%__MODULE__{}) do 157 | Logger.info("[Packet] Peer asked to disconnect for #{get_reason_msg(packet.reason) || packet.reason}.") 158 | 159 | :peer_disconnect 160 | end 161 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/get_block_bodies.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.GetBlockBodies do 2 | @moduledoc """ 3 | Request the bodies for a set of blocks by hash. 4 | 5 | ``` 6 | `GetBlockBodies` [`+0x05`, `hash_0`: `B_32`, `hash_1`: `B_32`, ...] 7 | 8 | Require peer to return a BlockBodies message. Specify the set of blocks that 9 | we're interested in with the hashes. 10 | ``` 11 | """ 12 | 13 | @behaviour ExWire.Packet 14 | 15 | @type t :: %__MODULE__{ 16 | hashes: [binary()] 17 | } 18 | 19 | defstruct [ 20 | hashes: [] 21 | ] 22 | 23 | @doc """ 24 | Given a GetBlockBodies packet, serializes for transport over Eth Wire Protocol. 25 | 26 | ## Examples 27 | 28 | iex> %ExWire.Packet.GetBlockBodies{hashes: [<<5>>, <<6>>]} 29 | ...> |> ExWire.Packet.GetBlockBodies.serialize 30 | [<<5>>, <<6>>] 31 | """ 32 | @spec serialize(t) :: ExRLP.t 33 | def serialize(packet=%__MODULE__{}) do 34 | packet.hashes 35 | end 36 | 37 | @doc """ 38 | Given an RLP-encoded GetBlockBodies packet from Eth Wire Protocol, 39 | decodes into a GetBlockBodies struct. 40 | 41 | ## Examples 42 | 43 | iex> ExWire.Packet.GetBlockBodies.deserialize([<<5>>, <<6>>]) 44 | %ExWire.Packet.GetBlockBodies{hashes: [<<5>>, <<6>>]} 45 | """ 46 | @spec deserialize(ExRLP.t) :: t 47 | def deserialize(rlp) do 48 | hashes = [_h|_t] = rlp # verify it's a list 49 | 50 | %__MODULE__{ 51 | hashes: hashes 52 | } 53 | end 54 | 55 | @doc """ 56 | Handles a GetBlockBodies message. We shoud send the block bodies 57 | to the peer if we have them. For now, we'll do nothing. 58 | 59 | ## Examples 60 | 61 | iex> %ExWire.Packet.GetBlockBodies{hashes: [<<5>>, <<6>>]} 62 | ...> |> ExWire.Packet.GetBlockBodies.handle() 63 | :ok 64 | """ 65 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 66 | def handle(_packet=%__MODULE__{}) do 67 | :ok 68 | end 69 | 70 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/get_block_headers.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.GetBlockHeaders do 2 | @moduledoc """ 3 | Requests block headers starting from a given hash. 4 | 5 | ``` 6 | **GetBlockHeaders** [`+0x03`: `P`, `block`: { `P` , `B_32` }, `maxHeaders`: `P`, `skip`: `P`, `reverse`: `P` in { `0` , `1` } ] 7 | Require peer to return a BlockHeaders message. Reply 8 | must contain a number of block headers, of rising number when reverse is 0, 9 | falling when 1, skip blocks apart, beginning at block block (denoted by either 10 | number or hash) in the canonical chain, and with at most maxHeaders items. 11 | ``` 12 | """ 13 | 14 | alias ExWire.Packet 15 | 16 | @behaviour ExWire.Packet 17 | 18 | @type t :: %__MODULE__{ 19 | block_identifier: Packet.block_identifier, 20 | max_headers: integer(), 21 | skip: integer(), 22 | reverse: boolean() 23 | } 24 | 25 | defstruct [ 26 | :block_identifier, 27 | :max_headers, 28 | :skip, 29 | :reverse 30 | ] 31 | 32 | @doc """ 33 | Given a GetBlockHeaders packet, serializes for transport over Eth Wire Protocol. 34 | 35 | ## Examples 36 | 37 | iex> %ExWire.Packet.GetBlockHeaders{block_identifier: 5, max_headers: 10, skip: 2, reverse: true} 38 | ...> |> ExWire.Packet.GetBlockHeaders.serialize 39 | [5, 10, 2, 1] 40 | 41 | iex> %ExWire.Packet.GetBlockHeaders{block_identifier: <<5>>, max_headers: 10, skip: 2, reverse: false} 42 | ...> |> ExWire.Packet.GetBlockHeaders.serialize 43 | [<<5>>, 10, 2, 0] 44 | """ 45 | @spec serialize(t) :: ExRLP.t 46 | def serialize(packet=%__MODULE__{}) do 47 | [ 48 | packet.block_identifier, 49 | packet.max_headers, 50 | packet.skip, 51 | (if packet.reverse, do: 1, else: 0) 52 | ] 53 | end 54 | 55 | @doc """ 56 | Given an RLP-encoded GetBlockHeaders packet from Eth Wire Protocol, 57 | decodes into a GetBlockHeaders struct. 58 | 59 | ## Examples 60 | 61 | iex> ExWire.Packet.GetBlockHeaders.deserialize([5, 10, 2, 1]) 62 | %ExWire.Packet.GetBlockHeaders{block_identifier: 5, max_headers: 10, skip: 2, reverse: true} 63 | 64 | iex> ExWire.Packet.GetBlockHeaders.deserialize([<<5>>, 10, 2, 0]) 65 | %ExWire.Packet.GetBlockHeaders{block_identifier: <<5>>, max_headers: 10, skip: 2, reverse: false} 66 | """ 67 | @spec deserialize(ExRLP.t) :: t 68 | def deserialize(rlp) do 69 | [ 70 | block_identifier, 71 | max_headers, 72 | skip, 73 | reverse 74 | ] = rlp 75 | 76 | %__MODULE__{ 77 | block_identifier: block_identifier, 78 | max_headers: max_headers, 79 | skip: skip, 80 | reverse: reverse == 1 81 | } 82 | end 83 | 84 | @doc """ 85 | Handles a GetBlockHeaders message. We shoud send the block headers 86 | to the peer if we have them. For now, we'll do nothing. 87 | 88 | ## Examples 89 | 90 | iex> %ExWire.Packet.GetBlockHeaders{block_identifier: 5, max_headers: 10, skip: 2, reverse: true} 91 | ...> |> ExWire.Packet.GetBlockHeaders.handle() 92 | :ok 93 | """ 94 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 95 | def handle(_packet=%__MODULE__{}) do 96 | :ok 97 | end 98 | 99 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/hello.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.Hello do 2 | @moduledoc """ 3 | This packet establishes capabilities and etc between two peer to peer 4 | clents. This is generally required to be the first signed packet communicated 5 | after the handshake is complete. 6 | 7 | ``` 8 | **Hello** `0x00` [`p2pVersion`: `P`, `clientId`: `B`, [[`cap1`: `B_3`, `capVersion1`: `P`], [`cap2`: `B_3`, `capVersion2`: `P`], ...], `listenPort`: `P`, `nodeId`: `B_64`] 9 | 10 | First packet sent over the connection, and sent once by both sides. No other messages 11 | may be sent until a `Hello` is received. 12 | 13 | * `p2pVersion` Specifies the implemented version of the P2P protocol. Now must be 1. 14 | * `clientId` Specifies the client software identity, as a human-readable string (e.g. "Ethereum(++)/1.0.0"). 15 | * `cap` Specifies a peer capability name as a length-3 ASCII string. Current supported capabilities are eth, shh. 16 | * `capVersion` Specifies a peer capability version as a positive integer. Current supported versions are 34 for eth, and 1 for shh. 17 | * `listenPort` specifies the port that the client is listening on (on the interface that the present connection traverses). If 0 it indicates the client is not listening. 18 | * `nodeId` is the Unique Identity of the node and specifies a 512-bit hash that identifies this node. 19 | ``` 20 | """ 21 | 22 | require Logger 23 | 24 | @behaviour ExWire.Packet 25 | 26 | @type cap :: {String.t, integer()} 27 | 28 | @type t :: %__MODULE__{ 29 | p2p_version: integer(), 30 | client_id: String.t, 31 | caps: [cap], 32 | listen_port: integer(), 33 | node_id: ExWire.node_id 34 | } 35 | 36 | defstruct [ 37 | :p2p_version, 38 | :client_id, 39 | :caps, 40 | :listen_port, 41 | :node_id 42 | ] 43 | 44 | @doc """ 45 | Given a Hello packet, serializes for transport over Eth Wire Protocol. 46 | 47 | ## Examples 48 | 49 | iex> %ExWire.Packet.Hello{p2p_version: 10, client_id: "Exthereum/Test", caps: [{"eth", 1}, {"par", 2}], listen_port: 5555, node_id: <<5>>} 50 | ...> |> ExWire.Packet.Hello.serialize 51 | ...> |> Enum.take(5) 52 | [10, "Exthereum/Test", [["eth", 1], ["par", 2]], 5555, <<5>>] 53 | """ 54 | @spec serialize(t) :: ExRLP.t 55 | def serialize(packet=%__MODULE__{}) do 56 | [ 57 | packet.p2p_version, 58 | packet.client_id, 59 | (for {cap, ver} <- packet.caps, do: [cap, ver]), 60 | packet.listen_port, 61 | packet.node_id, 62 | "#{ExWire.Config.local_ip() |> ExWire.Struct.Endpoint.ip_to_string}:#{ExWire.Config.listen_port()}" 63 | ] 64 | end 65 | 66 | @doc """ 67 | Given an RLP-encoded Hello packet from Eth Wire Protocol, 68 | decodes into a Hello struct. 69 | 70 | ## Examples 71 | 72 | iex> ExWire.Packet.Hello.deserialize([<<10>>, "Exthereum/Test", [["eth", <<1>>], ["par", <<2>>]], <<55>>, <<5>>]) 73 | %ExWire.Packet.Hello{p2p_version: 10, client_id: "Exthereum/Test", caps: [{"eth", 1}, {"par", 2}], listen_port: 55, node_id: <<5>>} 74 | """ 75 | @spec deserialize(ExRLP.t) :: t 76 | def deserialize(rlp) do 77 | [ 78 | p2p_version | 79 | [client_id | 80 | [caps | 81 | [listen_port | 82 | [node_id | 83 | _rest 84 | ]]]]] = rlp 85 | 86 | %__MODULE__{ 87 | p2p_version: p2p_version |> :binary.decode_unsigned, 88 | client_id: client_id, 89 | caps: (for [cap, ver] <- caps, do: {cap, ver |> :binary.decode_unsigned}), 90 | listen_port: listen_port |> :binary.decode_unsigned, 91 | node_id: node_id 92 | } 93 | end 94 | 95 | @doc """ 96 | Handles a Hello message. We can mark a peer as active for communication 97 | after we receive this message. 98 | 99 | ## Examples 100 | 101 | iex> %ExWire.Packet.Hello{p2p_version: 10, client_id: "Exthereum/Test", caps: [["eth", 1], ["par", 2]], listen_port: 5555, node_id: <<5>>} 102 | ...> |> ExWire.Packet.Hello.handle() 103 | :activate 104 | 105 | # When no caps 106 | iex> %ExWire.Packet.Hello{p2p_version: 10, client_id: "Exthereum/Test", caps: [], listen_port: 5555, node_id: <<5>>} 107 | ...> |> ExWire.Packet.Hello.handle() 108 | {:disconnect, :useless_peer} 109 | """ 110 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 111 | def handle(packet=%__MODULE__{}) do 112 | if System.get_env("TRACE"), do: Logger.debug("[Packet] Got Hello: #{inspect packet}") 113 | 114 | if packet.caps == [] do 115 | Logger.debug("[Packet] Disconnecting due to no matching peer caps (#{inspect packet.caps})") 116 | {:disconnect, :useless_peer} 117 | else 118 | 119 | # TODO: Add a bunch more checks 120 | :activate 121 | end 122 | end 123 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/new_block.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.NewBlock do 2 | @moduledoc """ 3 | Eth Wire Packet for advertising new blocks. 4 | 5 | ``` 6 | **NewBlock** [`+0x07`, [`blockHeader`, `transactionList`, `uncleList`], `totalDifficulty`] 7 | 8 | Specify a single block that the peer should know about. The composite item in 9 | the list (following the message ID) is a block in the format described in the 10 | main Ethereum specification. 11 | 12 | * `totalDifficulty` is the total difficulty of the block (aka score). 13 | ``` 14 | """ 15 | 16 | require Logger 17 | 18 | alias Block.Header 19 | alias ExWire.Struct.Block 20 | 21 | @behaviour ExWire.Packet 22 | 23 | @type t :: %__MODULE__{ 24 | block_header: Header.t, 25 | block: Block.t, 26 | total_difficulty: integer() 27 | } 28 | 29 | defstruct [ 30 | :block_header, 31 | :block, 32 | :total_difficulty 33 | ] 34 | 35 | @doc """ 36 | Given a NewBlock packet, serializes for transport over Eth Wire Protocol. 37 | 38 | ## Examples 39 | 40 | iex> %ExWire.Packet.NewBlock{ 41 | ...> block_header: %Block.Header{parent_hash: <<1::256>>, ommers_hash: <<2::256>>, beneficiary: <<3::160>>, state_root: <<4::256>>, transactions_root: <<5::256>>, receipts_root: <<6::256>>, logs_bloom: <<>>, difficulty: 5, number: 1, gas_limit: 5, gas_used: 3, timestamp: 6, extra_data: "Hi mom", mix_hash: <<7::256>>, nonce: <<8::64>>}, 42 | ...> block: %ExWire.Struct.Block{transaction_list: [], uncle_list: []}, 43 | ...> total_difficulty: 100_000 44 | ...> } 45 | ...> |> ExWire.Packet.NewBlock.serialize 46 | [ 47 | [<<1::256>>, <<2::256>>, <<3::160>>, <<4::256>>, <<5::256>>, <<6::256>>, <<>>, 5, 1, 5, 3, 6, "Hi mom", <<7::256>>, <<8::64>>], 48 | [], 49 | [], 50 | 100000 51 | ] 52 | """ 53 | @spec serialize(t) :: ExRLP.t 54 | def serialize(packet=%__MODULE__{}) do 55 | [trx_list, uncle_list] = Block.serialize(packet.block) 56 | 57 | [ 58 | Header.serialize(packet.block_header), 59 | trx_list, 60 | uncle_list, 61 | packet.total_difficulty 62 | ] 63 | end 64 | 65 | @doc """ 66 | Given an RLP-encoded NewBlock packet from Eth Wire Protocol, 67 | decodes into a NewBlock struct. 68 | 69 | ## Examples 70 | 71 | iex> [ 72 | ...> [<<1::256>>, <<2::256>>, <<3::160>>, <<4::256>>, <<5::256>>, <<6::256>>, <<>>, <<5>>, <<1>>, <<5>>, <<3>>, <<6>>, "Hi mom", <<7::256>>, <<8::64>>], 73 | ...> [], 74 | ...> [], 75 | ...> <<10>> 76 | ...> ] 77 | ...> |> ExWire.Packet.NewBlock.deserialize() 78 | %ExWire.Packet.NewBlock{ 79 | block_header: %Block.Header{parent_hash: <<1::256>>, ommers_hash: <<2::256>>, beneficiary: <<3::160>>, state_root: <<4::256>>, transactions_root: <<5::256>>, receipts_root: <<6::256>>, logs_bloom: <<>>, difficulty: 5, number: 1, gas_limit: 5, gas_used: 3, timestamp: 6, extra_data: "Hi mom", mix_hash: <<7::256>>, nonce: <<8::64>>}, 80 | block: %ExWire.Struct.Block{transaction_list: [], uncle_list: []}, 81 | total_difficulty: 10 82 | } 83 | """ 84 | @spec deserialize(ExRLP.t) :: t 85 | def deserialize(rlp) do 86 | [ 87 | block_header, 88 | trx_list, 89 | uncle_list, 90 | total_difficulty 91 | ] = rlp 92 | 93 | %__MODULE__{ 94 | block_header: Header.deserialize(block_header), 95 | block: Block.deserialize([trx_list, uncle_list]), 96 | total_difficulty: total_difficulty |> :binary.decode_unsigned 97 | } 98 | end 99 | 100 | @doc """ 101 | Handles a NewBlock message. Right now, we ignore these advertisements. 102 | 103 | ## Examples 104 | 105 | iex> %ExWire.Packet.NewBlock{ 106 | ...> block_header: %Block.Header{parent_hash: <<1::256>>, ommers_hash: <<2::256>>, beneficiary: <<3::160>>, state_root: <<4::256>>, transactions_root: <<5::256>>, receipts_root: <<6::256>>, logs_bloom: <<>>, difficulty: 5, number: 1, gas_limit: 5, gas_used: 3, timestamp: 6, extra_data: "Hi mom", mix_hash: <<7::256>>, nonce: <<8::64>>}, 107 | ...> block: %ExWire.Struct.Block{transaction_list: [], uncle_list: []}, 108 | ...> total_difficulty: 100_000 109 | ...> } 110 | ...> |> ExWire.Packet.NewBlock.handle() 111 | :ok 112 | """ 113 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 114 | def handle(packet=%__MODULE__{}) do 115 | Logger.debug("[Packet] Peer sent new block with hash #{packet.block_header |> Header.hash |> ExthCrypto.Math.bin_to_hex}") 116 | 117 | :ok 118 | end 119 | 120 | end 121 | -------------------------------------------------------------------------------- /lib/ex_wire/packet/new_block_hashes.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.NewBlockHashes do 2 | @moduledoc """ 3 | Advertises new blocks to the network. 4 | 5 | ``` 6 | **NewBlockHashes** [`+0x01`: `P`, [`hash_0`: `B_32`, `number_0`: `P`], [`hash_1`: `B_32`, `number_1`: `P`], ...] 7 | 8 | Specify one or more new blocks which have appeared on the 9 | network. To be maximally helpful, nodes should inform peers of all blocks that 10 | they may not be aware of. Including hashes that the sending peer could 11 | reasonably be considered to know (due to the fact they were previously 12 | informed of because that node has itself advertised knowledge of the hashes 13 | through NewBlockHashes) is considered Bad Form, and may reduce the reputation 14 | of the sending node. Including hashes that the sending node later refuses to 15 | honour with a proceeding GetBlockHeaders message is considered Bad Form, and 16 | may reduce the reputation of the sending node. 17 | ``` 18 | """ 19 | 20 | @behaviour ExWire.Packet 21 | 22 | @type t :: %__MODULE__{ 23 | hashes: [ExWire.Packet.block_hash] 24 | } 25 | 26 | defstruct [ 27 | :hashes 28 | ] 29 | 30 | @doc """ 31 | Given a NewBlockHashes packet, serializes for transport over Eth Wire Protocol. 32 | 33 | ## Examples 34 | 35 | iex> %ExWire.Packet.NewBlockHashes{hashes: [{<<5>>, 1}, {<<6>>, 2}]} 36 | ...> |> ExWire.Packet.NewBlockHashes.serialize() 37 | [[<<5>>, 1], [<<6>>, 2]] 38 | 39 | iex> %ExWire.Packet.NewBlockHashes{hashes: []} 40 | ...> |> ExWire.Packet.NewBlockHashes.serialize() 41 | [] 42 | """ 43 | @spec serialize(t) :: ExRLP.t 44 | def serialize(packet=%__MODULE__{}) do 45 | for {hash, number} <- packet.hashes, do: [hash, number] 46 | end 47 | 48 | @doc """ 49 | Given an RLP-encoded NewBlockHashes packet from Eth Wire Protocol, 50 | decodes into a NewBlockHashes struct. 51 | 52 | ## Examples 53 | 54 | iex> ExWire.Packet.NewBlockHashes.deserialize([[<<5>>, 1], [<<6>>, 2]]) 55 | %ExWire.Packet.NewBlockHashes{hashes: [{<<5>>, 1}, {<<6>>, 2}]} 56 | 57 | iex> ExWire.Packet.NewBlockHashes.deserialize([]) 58 | ** (MatchError) no match of right hand side value: [] 59 | """ 60 | @spec deserialize(ExRLP.t) :: t 61 | def deserialize(rlp) do 62 | hash_lists = [_h|_t] = rlp # must be an array with at least one element 63 | if Enum.count(hash_lists) > 256, do: raise "Too many hashes" 64 | 65 | hashes = for [hash, number] <- hash_lists, do: {hash, number} 66 | 67 | %__MODULE__{ 68 | hashes: hashes 69 | } 70 | end 71 | 72 | @doc """ 73 | Handles a NewBlockHashes message. This is when a peer wants to 74 | inform us that she knows about new blocks. For now, we'll do nothing. 75 | 76 | ## Examples 77 | 78 | iex> %ExWire.Packet.NewBlockHashes{hashes: [{<<5>>, 1}, {<<6>>, 2}]} 79 | ...> |> ExWire.Packet.NewBlockHashes.handle() 80 | :ok 81 | """ 82 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 83 | def handle(_packet=%__MODULE__{}) do 84 | # TODO: Do something 85 | 86 | :ok 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /lib/ex_wire/packet/ping.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.Ping do 2 | @moduledoc """ 3 | Ping is used to determine round-trip time of messages to a peer. 4 | 5 | ``` 6 | **Ping** `0x02` [] 7 | 8 | Requests an immediate reply of Pong from the peer. 9 | ``` 10 | """ 11 | 12 | require Logger 13 | 14 | @behaviour ExWire.Packet 15 | 16 | @type t :: %__MODULE__{} 17 | 18 | defstruct [] 19 | 20 | @doc """ 21 | Given a Ping packet, serializes for transport over Eth Wire Protocol. 22 | 23 | ## Examples 24 | 25 | iex> %ExWire.Packet.Ping{} 26 | ...> |> ExWire.Packet.Ping.serialize 27 | [] 28 | """ 29 | @spec serialize(t) :: ExRLP.t 30 | def serialize(_packet=%__MODULE__{}) do 31 | [] 32 | end 33 | 34 | @doc """ 35 | Given an RLP-encoded Ping packet from Eth Wire Protocol, 36 | decodes into a Ping struct. 37 | 38 | ## Examples 39 | 40 | iex> ExWire.Packet.Ping.deserialize([]) 41 | %ExWire.Packet.Ping{} 42 | """ 43 | @spec deserialize(ExRLP.t) :: t 44 | def deserialize(rlp) do 45 | [] = rlp 46 | 47 | %__MODULE__{} 48 | end 49 | 50 | @doc """ 51 | Handles a Ping message. We send a Pong back to the peer. 52 | 53 | ## Examples 54 | 55 | iex> ExWire.Packet.Ping.handle(%ExWire.Packet.Ping{}) 56 | {:send, %ExWire.Packet.Pong{}} 57 | """ 58 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 59 | def handle(_packet=%__MODULE__{}) do 60 | Logger.debug("[Packet] Received ping, responding pong.") 61 | 62 | {:send, %ExWire.Packet.Pong{}} 63 | end 64 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/pong.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.Pong do 2 | @moduledoc """ 3 | Pong is the response to a Ping message. 4 | 5 | ``` 6 | **Pong** `0x03` [] 7 | 8 | Reply to peer's `Ping` packet. 9 | ``` 10 | """ 11 | 12 | @behaviour ExWire.Packet 13 | 14 | @type t :: %__MODULE__{} 15 | 16 | defstruct [] 17 | 18 | @doc """ 19 | Given a Pong packet, serializes for transport over Eth Wire Protocol. 20 | 21 | ## Examples 22 | 23 | iex> %ExWire.Packet.Pong{} 24 | ...> |> ExWire.Packet.Pong.serialize 25 | [] 26 | """ 27 | @spec serialize(t) :: ExRLP.t 28 | def serialize(_packet=%__MODULE__{}) do 29 | [] 30 | end 31 | 32 | @doc """ 33 | Given an RLP-encoded Pong packet from Eth Wire Protocol, 34 | decodes into a Pong struct. 35 | 36 | ## Examples 37 | 38 | iex> ExWire.Packet.Pong.deserialize([]) 39 | %ExWire.Packet.Pong{} 40 | """ 41 | @spec deserialize(ExRLP.t) :: t 42 | def deserialize(rlp) do 43 | [] = rlp 44 | 45 | %__MODULE__{} 46 | end 47 | 48 | @doc """ 49 | Handles a Pong message. We should track the round-trip time since the 50 | corresponding Ping was sent to know how fast this peer is. 51 | 52 | ## Examples 53 | 54 | iex> ExWire.Packet.Pong.handle(%ExWire.Packet.Pong{}) 55 | :ok 56 | """ 57 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 58 | def handle(_packet=%__MODULE__{}) do 59 | # TODO: Track RTT time 60 | 61 | :ok 62 | end 63 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/status.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.Status do 2 | @moduledoc """ 3 | Status messages establish a proper Eth Wire connection, and verify the two clients are compatable. 4 | 5 | ``` 6 | **Status** [`+0x00`: `P`, `protocolVersion`: `P`, `networkId`: `P`, `td`: `P`, `bestHash`: `B_32`, `genesisHash`: `B_32`] 7 | 8 | Inform a peer of its current ethereum state. This message should be sent after the initial 9 | handshake and prior to any ethereum related messages. 10 | 11 | * `protocolVersion` is one of: 12 | * `0x00` for PoC-1; 13 | * `0x01` for PoC-2; 14 | * `0x07` for PoC-3; 15 | * `0x09` for PoC-4. 16 | * `0x17` for PoC-5. 17 | * `0x1c` for PoC-6. 18 | * `61` for PV61 19 | * `62` for PV62 20 | * `63` for PV63 21 | * `networkId`: 0=Olympic (disused), 1=Frontier (mainnet), 2=Morden (disused), 3=Ropsten (testnet), 4=Rinkeby 22 | * `td`: Total Difficulty of the best chain. Integer, as found in block header. 23 | * `bestHash`: The hash of the best (i.e. highest TD) known block. 24 | * `genesisHash`: The hash of the Genesis block. 25 | ``` 26 | """ 27 | 28 | require Logger 29 | 30 | @behaviour ExWire.Packet 31 | 32 | @type t :: %__MODULE__{ 33 | protocol_version: integer(), 34 | network_id: integer(), 35 | total_difficulty: integer(), 36 | best_hash: binary(), 37 | genesis_hash: binary(), 38 | manifest_hash: binary(), 39 | block_number: integer() 40 | } 41 | 42 | defstruct [ 43 | :protocol_version, 44 | :network_id, 45 | :total_difficulty, 46 | :best_hash, 47 | :genesis_hash, 48 | :manifest_hash, 49 | :block_number 50 | ] 51 | 52 | @doc """ 53 | Given a Status packet, serializes for transport over Eth Wire Protocol. 54 | 55 | ## Examples 56 | 57 | iex> %ExWire.Packet.Status{protocol_version: 0x63, network_id: 3, total_difficulty: 10, best_hash: <<5>>, genesis_hash: <<4>>} 58 | ...> |> ExWire.Packet.Status.serialize 59 | [0x63, 3, 10, <<5>>, <<4>>] 60 | """ 61 | @spec serialize(t) :: ExRLP.t 62 | def serialize(packet=%__MODULE__{}) do 63 | [ 64 | packet.protocol_version, 65 | packet.network_id, 66 | packet.total_difficulty, 67 | packet.best_hash, 68 | packet.genesis_hash 69 | ] 70 | end 71 | 72 | @doc """ 73 | Given an RLP-encoded Status packet from Eth Wire Protocol, decodes into a Status packet. 74 | 75 | Note: we will decode warp's `manifest_hash` and `block_number`, if given. 76 | 77 | ## Examples 78 | 79 | iex> ExWire.Packet.Status.deserialize([<<0x63>>, <<3>>, <<10>>, <<5>>, <<4>>]) 80 | %ExWire.Packet.Status{protocol_version: 0x63, network_id: 3, total_difficulty: 10, best_hash: <<5>>, genesis_hash: <<4>>} 81 | 82 | iex> ExWire.Packet.Status.deserialize([<<0x63>>, <<3>>, <<10>>, <<5>>, <<4>>, <<11>>, <<11>>]) 83 | %ExWire.Packet.Status{protocol_version: 0x63, network_id: 3, total_difficulty: 10, best_hash: <<5>>, genesis_hash: <<4>>, manifest_hash: <<11>>, block_number: 11} 84 | """ 85 | @spec deserialize(ExRLP.t) :: t 86 | def deserialize(rlp) do 87 | [ 88 | protocol_version | 89 | [network_id | 90 | [total_difficulty | 91 | [best_hash | 92 | [genesis_hash | 93 | rest 94 | ]]]]] = rlp 95 | 96 | {manifest_hash, block_number} = case rest do 97 | [] -> {nil, nil} 98 | [manifest_hash, block_number] -> {manifest_hash, block_number |> :binary.decode_unsigned} 99 | end 100 | 101 | %__MODULE__{ 102 | protocol_version: protocol_version |> :binary.decode_unsigned, 103 | network_id: network_id |> :binary.decode_unsigned, 104 | total_difficulty: total_difficulty |> :binary.decode_unsigned, 105 | best_hash: best_hash, 106 | genesis_hash: genesis_hash, 107 | manifest_hash: manifest_hash, 108 | block_number: block_number 109 | } 110 | end 111 | 112 | @doc """ 113 | Handles a Status message. 114 | 115 | We should decide whether or not we want to continue communicating with 116 | this peer. E.g. do our network and protocol versions match? 117 | 118 | ## Examples 119 | 120 | iex> %ExWire.Packet.Status{protocol_version: 63, network_id: 3, total_difficulty: 10, best_hash: <<5>>, genesis_hash: <<4>>} 121 | ...> |> ExWire.Packet.Status.handle() 122 | :ok 123 | 124 | # Test a peer with an incompatible version 125 | iex> %ExWire.Packet.Status{protocol_version: 555, network_id: 3, total_difficulty: 10, best_hash: <<5>>, genesis_hash: <<4>>} 126 | ...> |> ExWire.Packet.Status.handle() 127 | {:disconnect, :useless_peer} 128 | """ 129 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 130 | def handle(packet=%__MODULE__{}) do 131 | if System.get_env("TRACE"), do: Logger.debug("[Packet] Got Status: #{inspect packet}") 132 | 133 | unless packet.protocol_version == ExWire.Config.protocol_version do 134 | # TODO: We need to follow up on disconnection packets with disconnection ourselves 135 | Logger.debug("[Packet] Disconnecting to due incompatible protocol version (them #{packet.protocol_version}, us: #{ExWire.Config.protocol_version})") 136 | {:disconnect, :useless_peer} 137 | else 138 | :ok 139 | end 140 | end 141 | end -------------------------------------------------------------------------------- /lib/ex_wire/packet/transactions.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.Transactions do 2 | @moduledoc """ 3 | Eth Wire Packet for communicating new transactions. 4 | 5 | ``` 6 | **Transactions** [`+0x02`: `P`, [`nonce`: `P`, `receivingAddress`: `B_20`, `value`: `P`, ...], ...] 7 | 8 | Specify (a) transaction(s) that the peer should make sure is included on 9 | its transaction queue. The items in the list (following the first item 0x12) 10 | are transactions in the format described in the main Ethereum specification. 11 | Nodes must not resend the same transaction to a peer in the same session. This 12 | packet must contain at least one (new) transaction. 13 | ``` 14 | """ 15 | 16 | require Logger 17 | 18 | @behaviour ExWire.Packet 19 | 20 | @type t :: %__MODULE__{ 21 | transactions: [any()] 22 | } 23 | 24 | defstruct [ 25 | :transactions 26 | ] 27 | 28 | @doc """ 29 | Given a Transactions packet, serializes for transport over Eth Wire Protocol. 30 | 31 | ## Examples 32 | 33 | iex> %ExWire.Packet.Transactions{ 34 | ...> transactions: [ 35 | ...> [1, 2, 3], 36 | ...> [4, 5, 6] 37 | ...> ] 38 | ...> } 39 | ...> |> ExWire.Packet.Transactions.serialize 40 | [ [1, 2, 3], [4, 5, 6] ] 41 | """ 42 | @spec serialize(t) :: ExRLP.t 43 | def serialize(packet=%__MODULE__{}) do 44 | packet.transactions # TODO: Serialize accurately 45 | end 46 | 47 | @doc """ 48 | Given an RLP-encoded Transactions packet from Eth Wire Protocol, 49 | decodes into a Tranasctions struct. 50 | 51 | ## Examples 52 | 53 | iex> ExWire.Packet.Transactions.deserialize([ [1, 2, 3], [4, 5, 6] ]) 54 | %ExWire.Packet.Transactions{ 55 | transactions: [ 56 | [1, 2, 3], 57 | [4, 5, 6], 58 | ] 59 | } 60 | """ 61 | @spec deserialize(ExRLP.t) :: t 62 | def deserialize(rlp) do 63 | # TODO: Deserialize from proper struct 64 | 65 | %__MODULE__{ 66 | transactions: rlp 67 | } 68 | end 69 | 70 | @doc """ 71 | Handles a Transactions message. We should try to add the transaction 72 | to a queue and process it. Or, right now, do nothing. 73 | 74 | ## Examples 75 | 76 | iex> %ExWire.Packet.Transactions{transactions: []} 77 | ...> |> ExWire.Packet.Transactions.handle() 78 | :ok 79 | """ 80 | @spec handle(ExWire.Packet.packet) :: ExWire.Packet.handle_response 81 | def handle(packet=%__MODULE__{}) do 82 | # TODO: Do. 83 | Logger.debug("[Packet] Peer sent #{Enum.count(packet.transactions)} transaction(s).") 84 | 85 | :ok 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /lib/ex_wire/peer_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.PeerSupervisor do 2 | @moduledoc """ 3 | The Peer Supervisor is responsible for maintaining a set of peer TCP connections. 4 | 5 | We should ask bootnodes for a set of potential peers via the Discovery Protocol, and then 6 | we can connect to those nodes. Currently, we just connect to the Bootnodes themselves. 7 | """ 8 | 9 | # TODO: We need to track and see which of these are up. We need to percolate messages on success. 10 | 11 | use Supervisor 12 | 13 | require Logger 14 | 15 | @name __MODULE__ 16 | 17 | def start_link(:ok) do 18 | Supervisor.start_link(__MODULE__, :ok, name: @name) 19 | end 20 | 21 | def init(:ok) do 22 | children = [ 23 | worker(ExWire.Adapter.TCP, [], restart: :transient) 24 | ] 25 | 26 | Supervisor.init(children, strategy: :simple_one_for_one) 27 | end 28 | 29 | @doc """ 30 | Sends a packet to all active TCP connections. This is useful when we want to, for instance, 31 | ask for a `GetBlockBody` from all peers for a given block hash. 32 | """ 33 | def send_packet(pid, packet) do 34 | # Send to all of the Supervisor's children... 35 | # ... not the best. 36 | 37 | for {_id, child, _type, _modules} <- Supervisor.which_children(pid) do 38 | # Children which are being restarted by not have a child_pid at this time. 39 | if is_pid(child), do: ExWire.Adapter.TCP.send_packet(child, packet) 40 | end 41 | end 42 | 43 | @doc """ 44 | Informs our peer supervisor a new neighbour that we should connect to. 45 | """ 46 | def connect(neighbour) do 47 | Logger.debug("[Peer Supervisor] Starting TCP connection to neighbour #{neighbour.endpoint.ip |> ExWire.Struct.Endpoint.ip_to_string}:#{neighbour.endpoint.tcp_port} (#{neighbour.node |> ExthCrypto.Math.bin_to_hex})") 48 | 49 | peer = ExWire.Struct.Peer.from_neighbour(neighbour) 50 | 51 | Supervisor.start_child(@name, [:outbound, peer, [{:server, ExWire.Sync}]]) 52 | end 53 | 54 | end -------------------------------------------------------------------------------- /lib/ex_wire/protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Protocol do 2 | @moduledoc """ 3 | Functions to handle encoding and decoding messages for 4 | over the wire transfer. 5 | """ 6 | 7 | alias ExWire.Crypto 8 | alias ExWire.Message 9 | 10 | @doc """ 11 | Encodes a given message by appending it to a hash of 12 | its contents. 13 | 14 | ## Examples 15 | 16 | iex> message = %ExWire.Message.Ping{ 17 | ...> version: 1, 18 | ...> from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 19 | ...> to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 20 | ...> timestamp: 4 21 | ...> } 22 | iex> ExWire.Protocol.encode(message, <<1::256>>) 23 | <<104, 61, 189, 241, 64, 191, 8, 245, 109, 115, 6, 97, 59, 110, 31, 152, 252, 24 | 162, 30, 138, 113, 255, 207, 36, 58, 151, 75, 222, 78, 137, 173, 70, 175, 25 | 219, 61, 45, 146, 70, 181, 105, 123, 166, 37, 216, 218, 140, 54, 18, 169, 21, 26 | 90, 15, 243, 105, 2, 101, 154, 148, 117, 74, 182, 40, 112, 46, 84, 245, 102, 27 | 67, 159, 38, 71, 218, 230, 40, 55, 83, 200, 180, 236, 192, 53, 50, 235, 198, 28 | 152, 152, 127, 241, 82, 7, 92, 202, 59, 197, 237, 102, 1, 1, 214, 1, 201, 29 | 132, 1, 2, 3, 4, 128, 130, 0, 5, 201, 132, 5, 6, 7, 8, 130, 0, 6, 128, 4>> 30 | """ 31 | @spec encode(Message.t, Crypto.private_key) :: binary() 32 | def encode(message, private_key) do 33 | signed_message = sign_message(message, private_key) 34 | 35 | Crypto.hash(signed_message) <> signed_message 36 | end 37 | 38 | @doc """ 39 | Returns a signed version of a message. This encodes 40 | the message type, the encoded message itself, and a signature 41 | for the message. 42 | 43 | ## Examples 44 | 45 | iex> message = %ExWire.Message.Ping{ 46 | ...> version: 1, 47 | ...> from: %ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, tcp_port: 5, udp_port: nil}, 48 | ...> to: %ExWire.Struct.Endpoint{ip: {5, 6, 7, 8}, tcp_port: nil, udp_port: 6}, 49 | ...> timestamp: 4 50 | ...> } 51 | iex> signature = ExWire.Protocol.sign_message(message, ExthCrypto.Test.private_key()) 52 | iex> ExthCrypto.Signature.verify(message |> ExWire.Message.encode |> ExWire.Crypto.hash, signature, ExthCrypto.Test.public_key()) 53 | true 54 | """ 55 | @spec sign_message(Message.t, Crypto.private_key) :: binary() 56 | def sign_message(message, private_key) do 57 | message 58 | |> Message.encode() 59 | |> sign_binary(private_key) 60 | end 61 | 62 | @doc """ 63 | Given a binary, returns an signed version encoded into a 64 | binary with signature, recovery id and the message itself. 65 | 66 | ## Examples 67 | 68 | iex> signature = ExWire.Protocol.sign_binary("mace windu", ExthCrypto.Test.private_key()) 69 | iex> ExthCrypto.Signature.verify(ExWire.Crypto.hash("mace windu"), signature, ExthCrypto.Test.public_key()) 70 | true 71 | """ 72 | @spec sign_binary(binary(), Crypto.private_key) :: binary() 73 | def sign_binary(value, private_key) do 74 | hashed_value = Crypto.hash(value) 75 | 76 | {signature, _r, _s, recovery_id} = ExthCrypto.Signature.sign_digest(hashed_value, private_key) 77 | 78 | signature <> <> <> value 79 | end 80 | 81 | end -------------------------------------------------------------------------------- /lib/ex_wire/struct/block.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.Block do 2 | @moduledoc """ 3 | A struct for storing blocks as they are transported over the Eth Wire Protocol. 4 | """ 5 | 6 | defstruct [ 7 | :transactions_list, 8 | :transactions, 9 | :ommers 10 | ] 11 | 12 | @type t :: %__MODULE__{ 13 | transactions: [Blockchain.Transaction.t], 14 | ommers: [binary()] 15 | } 16 | 17 | @doc """ 18 | Given a Block, serializes for transport over Eth Wire Protocol. 19 | 20 | ## Examples 21 | 22 | iex> %ExWire.Struct.Block{transactions_list: [[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], ommers: [<<1::256>>]} 23 | ...> |> ExWire.Struct.Block.serialize 24 | [[[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], [<<1::256>>]] 25 | """ 26 | @spec serialize(t) :: ExRLP.t 27 | def serialize(struct) do 28 | [ 29 | struct.transactions_list, 30 | struct.ommers 31 | ] 32 | end 33 | 34 | @doc """ 35 | Given an RLP-encoded block from Eth Wire Protocol, decodes into a Block struct. 36 | 37 | ## Examples 38 | 39 | iex> ExWire.Struct.Block.deserialize([[[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], [<<1::256>>]]) 40 | %ExWire.Struct.Block{ 41 | transactions_list: [[<<5>>, <<6>>, <<7>>, <<1::160>>, <<8>>, "hi", <<27>>, <<9>>, <<10>>]], 42 | transactions: [%Blockchain.Transaction{nonce: 5, gas_price: 6, gas_limit: 7, to: <<1::160>>, value: 8, v: 27, r: 9, s: 10, data: "hi"}], 43 | ommers: [<<1::256>>] 44 | } 45 | """ 46 | @spec deserialize(ExRLP.t) :: t 47 | def deserialize(rlp) do 48 | [ 49 | transactions_list, 50 | ommers 51 | ] = rlp 52 | 53 | %__MODULE__{ 54 | transactions_list: transactions_list, 55 | transactions: ( for transaction_rlp <- transactions_list, do: Blockchain.Transaction.deserialize(transaction_rlp) ), 56 | ommers: ommers 57 | } 58 | end 59 | 60 | end -------------------------------------------------------------------------------- /lib/ex_wire/struct/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.Endpoint do 2 | @moduledoc """ 3 | Struct to represent an endpoint in ExWire. 4 | """ 5 | 6 | defstruct [ 7 | ip: nil, 8 | udp_port: nil, 9 | tcp_port: nil 10 | ] 11 | 12 | @type ip :: :inet.ip_address 13 | @type ip_port :: integer() 14 | 15 | @type t :: %__MODULE__{ 16 | ip: ip, 17 | udp_port: ip_port | nil, 18 | tcp_port: ip_port | nil 19 | } 20 | 21 | @doc """ 22 | Returns a struct given an `ip` in binary form, plus an 23 | `udp_port` or `tcp_port`. 24 | 25 | ## Examples 26 | 27 | iex> ExWire.Struct.Endpoint.decode([<<1,2,3,4>>, <<>>, <<5>>]) 28 | %ExWire.Struct.Endpoint{ 29 | ip: {1, 2, 3, 4}, 30 | udp_port: nil, 31 | tcp_port: 5, 32 | } 33 | """ 34 | @spec decode(ExRLP.t) :: t 35 | def decode([ip, udp_port, tcp_port]) do 36 | %__MODULE__{ 37 | ip: decode_ip(ip), 38 | udp_port: decode_port(udp_port), 39 | tcp_port: decode_port(tcp_port), 40 | } 41 | end 42 | 43 | @doc """ 44 | Given an IPv4 or IPv6 address in binary form, 45 | returns the address in list form. 46 | 47 | ## Examples 48 | 49 | iex> ExWire.Struct.Endpoint.decode_ip(<<1,2,3,4>>) 50 | {1, 2, 3, 4} 51 | 52 | iex> ExWire.Struct.Endpoint.decode_ip(<<1::128>>) 53 | {0, 0, 0, 0, 0, 0, 0, 1} 54 | 55 | iex> ExWire.Struct.Endpoint.decode_ip(<<0xff, 0xff, 0xff, 0xff>>) 56 | {255, 255, 255, 255} 57 | 58 | iex> ExWire.Struct.Endpoint.decode_ip(<<127, 0, 0, 1>>) 59 | {127, 0, 0, 1} 60 | """ 61 | @spec decode_ip(binary()) :: ip 62 | def decode_ip(data) do 63 | case data do 64 | <<>> -> {} 65 | <> -> {p_0, p_1, p_2, p_3} 66 | <> -> {p_0, p_1, p_2, p_3, p_4, p_5, p_6, p_7} 68 | end 69 | end 70 | 71 | @doc """ 72 | Returns a port given a binary version of the port 73 | as input. Note: we return `nil` for an empty or zero binary. 74 | 75 | ## Examples 76 | 77 | iex> ExWire.Struct.Endpoint.decode_port(<<>>) 78 | nil 79 | 80 | iex> ExWire.Struct.Endpoint.decode_port(<<0>>) 81 | nil 82 | 83 | iex> ExWire.Struct.Endpoint.decode_port(<<0, 0>>) 84 | nil 85 | 86 | iex> ExWire.Struct.Endpoint.decode_port(<<1>>) 87 | 1 88 | 89 | iex> ExWire.Struct.Endpoint.decode_port(<<1, 0>>) 90 | 256 91 | """ 92 | def decode_port(data) do 93 | case :binary.decode_unsigned(data) do 94 | 0 -> nil 95 | port -> port 96 | end 97 | end 98 | 99 | @doc """ 100 | Versus `decode/3`, and given a module with an ip, a tcp_port and 101 | a udp_port, returns a tuple of encoded values. 102 | 103 | ## Examples 104 | 105 | iex> ExWire.Struct.Endpoint.encode(%ExWire.Struct.Endpoint{ip: {1, 2, 3, 4}, udp_port: nil, tcp_port: 5}) 106 | [<<1, 2, 3, 4>>, <<>>, <<0, 5>>] 107 | """ 108 | @spec encode(t) :: ExRLP.t 109 | def encode(%__MODULE__{ip: ip, tcp_port: tcp_port, udp_port: udp_port}) do 110 | [ 111 | encode_ip(ip), 112 | encode_port(udp_port), 113 | encode_port(tcp_port), 114 | ] 115 | end 116 | 117 | @doc """ 118 | Given an ip address that's an encoded as a list, 119 | returns that address encoded as a binary. 120 | 121 | ## Examples 122 | 123 | iex> ExWire.Struct.Endpoint.encode_ip({1, 2, 3, 4}) 124 | <<1, 2, 3, 4>> 125 | 126 | iex> ExWire.Struct.Endpoint.encode_ip({0, 0, 0, 0, 0, 0, 0, 1}) 127 | <<1::128>> 128 | 129 | iex> ExWire.Struct.Endpoint.encode_ip({0xffff, 0x0000, 0xff00, 0x0000, 0x2233, 0x00ff, 0x1122, 0x3344}) 130 | <<255, 255, 0, 0, 255, 0, 0, 0, 34, 51, 0, 255, 17, 34, 51, 68>> 131 | """ 132 | @spec encode_ip(ip) :: binary() 133 | def encode_ip(ip) do 134 | case ip do 135 | {p_0, p_1, p_2, p_3} -> <> 136 | {p_0, p_1, p_2, p_3, p_4, p_5, p_6, p_7} -> 137 | encode_word(p_0) <> 138 | encode_word(p_1) <> 139 | encode_word(p_2) <> 140 | encode_word(p_3) <> 141 | encode_word(p_4) <> 142 | encode_word(p_5) <> 143 | encode_word(p_6) <> 144 | encode_word(p_7) 145 | end 146 | end 147 | 148 | @doc """ 149 | Given a port, returns that port encoded in binary. 150 | 151 | ## Examples 152 | 153 | iex> ExWire.Struct.Endpoint.encode_port(256) 154 | <<1, 0>> 155 | 156 | iex> ExWire.Struct.Endpoint.encode_port(nil) 157 | <<>> 158 | 159 | iex> ExWire.Struct.Endpoint.encode_port(0) 160 | <<0, 0>> 161 | """ 162 | @spec encode_port(ip_port | nil) :: binary() 163 | def encode_port(port) do 164 | case port do 165 | nil -> <<>> 166 | _ -> port |> :binary.encode_unsigned |> ExthCrypto.Math.pad(2) 167 | end 168 | end 169 | 170 | @doc """ 171 | Returns a string representing the IP address. 172 | 173 | ## Examples 174 | 175 | iex> ExWire.Struct.Endpoint.ip_to_string({1, 2, 3, 4}) 176 | "1.2.3.4" 177 | 178 | iex> ExWire.Struct.Endpoint.ip_to_string({0, 0, 0, 0, 0, 0, 0, 1}) 179 | "::1" 180 | 181 | iex> ExWire.Struct.Endpoint.ip_to_string({0xffff, 0xffff, 0xff00, 0xff00, 0x00ff, 0x00ff, 0x1122, 0x1122}) 182 | "ffff:ffff:ff00:ff00:ff:ff:1122:1122" 183 | """ 184 | def ip_to_string(ip) do 185 | :inet_parse.ntoa(ip) |> to_string 186 | end 187 | 188 | defp encode_word(word) do 189 | case word |> :binary.encode_unsigned do 190 | <> -> <<0>> <> single_byte 191 | <> -> double_byte 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /lib/ex_wire/struct/neighbour.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.Neighbour do 2 | @moduledoc """ 3 | Struct to represent an neighbour in RLPx. 4 | """ 5 | 6 | alias ExWire.Struct.Endpoint 7 | 8 | defstruct [ 9 | endpoint: nil, 10 | node: nil 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | endpoint: ExWire.Struct.Endpoint.t, 15 | node: ExWire.node_id, 16 | } 17 | 18 | @doc """ 19 | Returns an Neighbour based on a URI. 20 | 21 | ## Examples 22 | 23 | iex> ExWire.Struct.Neighbour.from_uri("enode://6ce05930c72abc632c58e2e4324f7c7ea478cec0ed4fa2528982cf34483094e9cbc9216e7aa349691242576d552a2a56aaeae426c5303ded677ce455ba1acd9d@13.84.180.240:30303") 24 | {:ok, %ExWire.Struct.Neighbour{ 25 | endpoint: %ExWire.Struct.Endpoint{ 26 | ip: {13, 84, 180, 240}, 27 | tcp_port: 30303, 28 | udp_port: 30303, 29 | }, 30 | node: <<108, 224, 89, 48, 199, 42, 188, 99, 44, 88, 226, 228, 50, 79, 124, 126, 164, 120, 206, 192, 237, 79, 162, 82, 137, 130, 207, 52, 72, 48, 148, 233, 203, 201, 33, 110, 122, 163, 73, 105, 18, 66, 87, 109, 85, 42, 42, 86, 170, 234, 228, 38, 197, 48, 61, 237, 103, 124, 228, 85, 186, 26, 205, 157>> 31 | }} 32 | 33 | iex> ExWire.Struct.Neighbour.from_uri("http://google:30303") 34 | {:error, :invalid_scheme} 35 | 36 | iex> ExWire.Struct.Neighbour.from_uri("abc") 37 | {:error, :invalid_uri} 38 | """ 39 | @spec from_uri(String.t) :: {:ok, t} | {:error, atom()} 40 | def from_uri(uri) do 41 | case URI.parse(uri) do 42 | %URI{ 43 | scheme: "enode", 44 | userinfo: remote_id, 45 | host: remote_host, 46 | port: remote_peer_port 47 | } -> 48 | {:ok, remote_ip} = :inet.ip(remote_host |> String.to_charlist) 49 | 50 | {:ok, %ExWire.Struct.Neighbour{ 51 | endpoint: %ExWire.Struct.Endpoint{ 52 | ip: remote_ip, 53 | udp_port: remote_peer_port, 54 | tcp_port: remote_peer_port, 55 | }, 56 | node: remote_id |> ExthCrypto.Math.hex_to_bin 57 | }} 58 | %URI{scheme: nil} -> {:error, :invalid_uri} 59 | %URI{} -> {:error, :invalid_scheme} 60 | end 61 | end 62 | 63 | @doc """ 64 | Returns a struct given an `ip` in binary form, plus an 65 | `udp_port` or `tcp_port`, along with a `node_id`, returns 66 | a `Neighbour` struct. 67 | 68 | ## Examples 69 | 70 | iex> ExWire.Struct.Neighbour.decode([<<1,2,3,4>>, <<>>, <<5>>, <<7, 7>>]) 71 | %ExWire.Struct.Neighbour{ 72 | endpoint: %ExWire.Struct.Endpoint{ 73 | ip: {1, 2, 3, 4}, 74 | udp_port: nil, 75 | tcp_port: 5, 76 | }, 77 | node: <<7, 7>> 78 | } 79 | """ 80 | @spec decode(ExRLP.t) :: t 81 | def decode([ip, udp_port, tcp_port, node_id]) do 82 | %__MODULE__{ 83 | endpoint: Endpoint.decode([ip, udp_port, tcp_port]), 84 | node: node_id, 85 | } 86 | end 87 | 88 | @doc """ 89 | Versus `encode/4`, and given a module with an ip, a tcp_port, a udp_port, 90 | and a node_id, returns a tuple of encoded values. 91 | 92 | ## Examples 93 | 94 | iex> ExWire.Struct.Neighbour.encode( 95 | ...> %ExWire.Struct.Neighbour{ 96 | ...> endpoint: %ExWire.Struct.Endpoint{ 97 | ...> ip: {1, 2, 3, 4}, 98 | ...> udp_port: nil, 99 | ...> tcp_port: 5, 100 | ...> }, 101 | ...> node: <<7, 8>>, 102 | ...> } 103 | ...> ) 104 | [<<1, 2, 3, 4>>, <<>>, <<0, 5>>, <<7, 8>>] 105 | """ 106 | @spec encode(t) :: ExRLP.t 107 | def encode(%__MODULE__{endpoint: endpoint, node: node_id}) do 108 | Endpoint.encode(endpoint) ++ [node_id] 109 | end 110 | 111 | end -------------------------------------------------------------------------------- /lib/ex_wire/struct/peer.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.Peer do 2 | @moduledoc """ 3 | Represents a Peer for an RLPx / Eth Wire connection. 4 | """ 5 | 6 | defstruct [ 7 | :host, 8 | :port, 9 | :remote_id, 10 | :ident 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | host: String.t, 15 | port: integer(), 16 | remote_id: String.t, 17 | ident: String.t, 18 | } 19 | 20 | @doc """ 21 | Constructs a new Peer struct. 22 | 23 | ## Examples 24 | 25 | iex> ExWire.Struct.Peer.new("13.84.180.240", 30303, "6ce05930c72abc632c58e2e4324f7c7ea478cec0ed4fa2528982cf34483094e9cbc9216e7aa349691242576d552a2a56aaeae426c5303ded677ce455ba1acd9d") 26 | %ExWire.Struct.Peer{ 27 | host: "13.84.180.240", 28 | port: 30303, 29 | remote_id: <<4, 108, 224, 89, 48, 199, 42, 188, 99, 44, 88, 226, 228, 50, 79, 124, 126, 164, 120, 206, 192, 237, 79, 162, 82, 137, 130, 207, 52, 72, 48, 148, 233, 203, 201, 33, 110, 122, 163, 73, 105, 18, 66, 87, 109, 85, 42, 42, 86, 170, 234, 228, 38, 197, 48, 61, 237, 103, 124, 228, 85, 186, 26, 205, 157>>, 30 | ident: "6ce059...1acd9d" 31 | } 32 | """ 33 | @spec new(Sring.t, integer(), String.t) :: t 34 | def new(host, port, remote_id_hex) do 35 | remote_id = remote_id_hex |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 36 | ident = Binary.take(remote_id_hex, 6) <> "..." <> Binary.take(remote_id_hex, -6) 37 | 38 | %__MODULE__{ 39 | host: host, 40 | port: port, 41 | remote_id: remote_id, 42 | ident: ident 43 | } 44 | end 45 | 46 | @doc """ 47 | Constructs a new Peer struct from a Neighbour struct. 48 | 49 | ## Examples 50 | 51 | iex> %ExWire.Struct.Neighbour{ 52 | ...> endpoint: %ExWire.Struct.Endpoint{ 53 | ...> ip: {13, 84, 180, 240}, 54 | ...> tcp_port: 30303, 55 | ...> }, 56 | ...> node: <<108, 224, 89, 48, 199, 42, 188, 99, 44, 88, 226, 228, 50, 79, 124, 126, 164, 120, 206, 192, 237, 79, 162, 82, 137, 130, 207, 52, 72, 48, 148, 233, 203, 201, 33, 110, 122, 163, 73, 105, 18, 66, 87, 109, 85, 42, 42, 86, 170, 234, 228, 38, 197, 48, 61, 237, 103, 124, 228, 85, 186, 26, 205, 157>> 57 | ...> } 58 | ...> |> ExWire.Struct.Peer.from_neighbour() 59 | %ExWire.Struct.Peer{ 60 | host: "13.84.180.240", 61 | port: 30303, 62 | remote_id: <<4, 108, 224, 89, 48, 199, 42, 188, 99, 44, 88, 226, 228, 50, 79, 124, 126, 164, 120, 206, 192, 237, 79, 162, 82, 137, 130, 207, 52, 72, 48, 148, 233, 203, 201, 33, 110, 122, 163, 73, 105, 18, 66, 87, 109, 85, 42, 42, 86, 170, 234, 228, 38, 197, 48, 61, 237, 103, 124, 228, 85, 186, 26, 205, 157>>, 63 | ident: "6ce059...1acd9d" 64 | } 65 | """ 66 | def from_neighbour(neighbour) do 67 | new(neighbour.endpoint.ip |> ExWire.Struct.Endpoint.ip_to_string, neighbour.endpoint.tcp_port, neighbour.node |> ExthCrypto.Math.bin_to_hex) 68 | end 69 | 70 | @doc """ 71 | Constructs a peer from a URI. 72 | 73 | ## Examples 74 | 75 | iex> ExWire.Struct.Peer.from_uri("enode://6ce05930c72abc632c58e2e4324f7c7ea478cec0ed4fa2528982cf34483094e9cbc9216e7aa349691242576d552a2a56aaeae426c5303ded677ce455ba1acd9d@13.84.180.240:30303") 76 | {:ok, %ExWire.Struct.Peer{ 77 | host: "13.84.180.240", 78 | port: 30303, 79 | remote_id: <<4, 108, 224, 89, 48, 199, 42, 188, 99, 44, 88, 226, 228, 50, 79, 124, 126, 164, 120, 206, 192, 237, 79, 162, 82, 137, 130, 207, 52, 72, 48, 148, 233, 203, 201, 33, 110, 122, 163, 73, 105, 18, 66, 87, 109, 85, 42, 42, 86, 170, 234, 228, 38, 197, 48, 61, 237, 103, 124, 228, 85, 186, 26, 205, 157>>, 80 | ident: "6ce059...1acd9d" 81 | }} 82 | 83 | iex> ExWire.Struct.Peer.from_uri("http://id@google.com:30303") 84 | {:error, "URI scheme must be enode, got http"} 85 | 86 | iex> ExWire.Struct.Peer.from_uri("abc") 87 | {:error, "Invalid URI"} 88 | """ 89 | @spec from_uri(String.t) :: {:ok, t} | {:error, String.t} 90 | def from_uri(uri) do 91 | case URI.parse(uri) do 92 | %URI{ 93 | scheme: "enode", 94 | userinfo: remote_id_hex, 95 | host: host, 96 | port: port 97 | } -> 98 | {:ok, __MODULE__.new(host, port, remote_id_hex)} 99 | %URI{scheme: nil} -> {:error, "Invalid URI"} 100 | %URI{scheme: scheme} -> {:error, "URI scheme must be enode, got #{scheme}"} 101 | end 102 | end 103 | end 104 | 105 | defimpl String.Chars, for: ExWire.Struct.Peer do 106 | 107 | @spec to_string(ExWire.Struct.Peer.t) :: String.t 108 | def to_string(peer) do 109 | peer.ident 110 | end 111 | 112 | end -------------------------------------------------------------------------------- /lib/ex_wire/sync.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Sync do 2 | @moduledoc """ 3 | This is the heart of our syncing logic. Once we've connected to a number 4 | of peers via `ExWire.PeerSup`, we begin to ask for new blocks from those 5 | peers. As we receive blocks, we add them to our `ExWire.Struct.BlockQueue`. 6 | If the blocks are confirmed by enough peers, then we verify the block and 7 | add it to our block tree. 8 | 9 | Note: we do not currently store the block tree, and thus we need to build 10 | it from genesis each time. 11 | """ 12 | use GenServer 13 | 14 | require Logger 15 | 16 | alias Block.Header 17 | alias ExWire.Struct.BlockQueue 18 | alias ExWire.Packet.BlockHeaders 19 | alias ExWire.Packet.BlockBodies 20 | alias ExWire.PeerSupervisor 21 | 22 | @doc """ 23 | Starts a Sync process. 24 | """ 25 | def start_link(db) do 26 | GenServer.start_link(__MODULE__, db, name: ExWire.Sync) 27 | end 28 | 29 | @doc """ 30 | Once we start a sync server, we'll wait for active peers. 31 | 32 | TODO: We do not always want to sync from the genesis. 33 | We will need to add some "restore state" logic. 34 | """ 35 | def init(db) do 36 | block_tree = Blockchain.Blocktree.new_tree() 37 | 38 | {:ok, %{ 39 | block_queue: %BlockQueue{}, 40 | block_tree: block_tree, 41 | chain: ExWire.Config.chain(), 42 | db: db, 43 | last_requested_block: request_next_block(block_tree) 44 | }} 45 | end 46 | 47 | @doc """ 48 | When were receive a block header, we'll add it to our block queue. When we receive the corresponding block body, 49 | we'll add that as well. 50 | """ 51 | def handle_info({:packet, %BlockHeaders{}=block_headers, peer}, state=%{block_queue: block_queue, block_tree: block_tree, chain: chain, db: db, last_requested_block: last_requested_block}) do 52 | {next_block_queue, next_block_tree} = Enum.reduce(block_headers.headers, {block_queue, block_tree}, fn header, {block_queue, block_tree} -> 53 | header_hash = header |> Header.hash 54 | 55 | {block_queue, block_tree, should_request_block} = BlockQueue.add_header_to_block_queue(block_queue, block_tree, header, header_hash, peer.remote_id, chain, db) 56 | 57 | if should_request_block do 58 | Logger.debug("[Sync] Requesting block body #{header.number}") 59 | 60 | # TODO: Bulk up these requests? 61 | PeerSupervisor.send_packet(PeerSupervisor, %ExWire.Packet.GetBlockBodies{hashes: [header_hash]}) 62 | end 63 | 64 | {block_queue, block_tree} 65 | end) 66 | 67 | # We can make this better, but it's basically "if we change, request another block" 68 | new_last_requested_block = if next_block_tree.parent_map != block_tree.parent_map do 69 | request_next_block(next_block_tree) 70 | else 71 | last_requested_block 72 | end 73 | 74 | {:noreply, 75 | state 76 | |> Map.put(:block_queue, next_block_queue) 77 | |> Map.put(:block_tree, next_block_tree) 78 | |> Map.put(:last_requested_block, new_last_requested_block) 79 | } 80 | end 81 | 82 | def handle_info({:packet, %BlockBodies{}=block_bodies, _peer}, state=%{block_queue: block_queue, block_tree: block_tree, chain: chain, db: db, last_requested_block: last_requested_block}) do 83 | {next_block_queue, next_block_tree} = Enum.reduce(block_bodies.blocks, {block_queue, block_tree}, fn block_body, {block_queue, block_tree} -> 84 | BlockQueue.add_block_struct_to_block_queue(block_queue, block_tree, block_body, chain, db) 85 | end) 86 | 87 | # We can make this better, but it's basically "if we change, request another block" 88 | new_last_requested_block = if next_block_tree.parent_map != block_tree.parent_map do 89 | request_next_block(next_block_tree) 90 | else 91 | last_requested_block 92 | end 93 | 94 | {:noreply, 95 | state 96 | |> Map.put(:block_queue, next_block_queue) 97 | |> Map.put(:block_tree, next_block_tree) 98 | |> Map.put(:last_requested_block, new_last_requested_block) 99 | } 100 | end 101 | 102 | def handle_info({:packet, packet, peer}, state) do 103 | Logger.debug("[Sync] Ignoring packet #{packet.__struct__} from #{peer}") 104 | 105 | {:noreply, state} 106 | end 107 | 108 | def request_next_block(block_tree) do 109 | next_number = case Blockchain.Blocktree.get_canonical_block(block_tree) do 110 | :root -> 0 111 | %Blockchain.Block{header: %Block.Header{number: number}} -> number + 1 112 | end 113 | 114 | Logger.debug("[Sync] Requesting block #{next_number}") 115 | 116 | ExWire.PeerSupervisor.send_packet(ExWire.PeerSupervisor, %ExWire.Packet.GetBlockHeaders{block_identifier: next_number, max_headers: 1, skip: 0, reverse: false}) 117 | 118 | next_number 119 | end 120 | 121 | end -------------------------------------------------------------------------------- /lib/ex_wire/util/timestamp.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Util.Timestamp do 2 | @moduledoc """ 3 | Helper functions for getting current time. 4 | """ 5 | 6 | @expiration 20 # seconds 7 | 8 | @doc """ 9 | Returns the current time as a unix epoch. 10 | """ 11 | @spec now() :: integer() 12 | def now do 13 | :os.system_time(:seconds) 14 | end 15 | 16 | @doc """ 17 | Returns the current time plus a global expiration. 18 | """ 19 | @spec soon() :: integer() 20 | def soon do 21 | now() + @expiration 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :ex_wire, 6 | version: "0.1.2", 7 | elixir: "~> 1.6", 8 | description: "Elixir Client for Ethereum's RLPx, DevP2P and Eth Wire Protocol", 9 | package: [ 10 | maintainers: ["Mason Fischer", "Geoffrey Hayes", "Ayrat Badykov"], 11 | licenses: ["MIT"], 12 | links: %{"GitHub" => "https://github.com/exthereum/ex_wire"} 13 | ], 14 | elixirc_paths: elixirc_paths(Mix.env), 15 | build_embedded: Mix.env == :prod, 16 | start_permanent: Mix.env == :prod, 17 | deps: deps()] 18 | end 19 | 20 | def application do 21 | [mod: {ExWire, []}, 22 | extra_applications: [:logger]] 23 | end 24 | 25 | defp elixirc_paths(:test), do: ["lib", "test/support"] 26 | defp elixirc_paths(_), do: ["lib"] 27 | 28 | defp deps do 29 | [ 30 | {:credo, "~> 0.9.1", only: [:dev, :test], runtime: false}, 31 | {:ex_doc, "~> 0.16", only: :dev, runtime: false}, 32 | {:ex_rlp, "~> 0.2.1"}, 33 | {:blockchain, "~> 0.1.7"}, 34 | {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, 35 | {:exth_crypto, "~> 0.1.6"}, 36 | {:evm, "~> 0.1.11"}, 37 | {:nat_upnp, "~> 0.1.0"} 38 | ] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "binary": {:hex, :binary, "0.0.5", "20d816f7274ea34f1b673b4cff2fdb9ebec9391a7a68c349070d515c66b1b2cf", [:mix], [], "hexpm"}, 3 | "blockchain": {:hex, :blockchain, "0.1.7", "6efdadb84242cbbe201f78ab3a472b2a155c1e86b8787eeb7fbc83ea58f94b99", [:mix], [{:evm, "~> 0.1.14", [hex: :evm, repo: "hexpm", optional: false]}, {:ex_rlp, "~> 0.2.1", [hex: :ex_rlp, repo: "hexpm", optional: false]}, {:keccakf1600, "~> 2.0.0", [hex: :keccakf1600_orig, repo: "hexpm", optional: false]}, {:libsecp256k1, "~> 0.1.4", [hex: :libsecp256k1, repo: "hexpm", optional: false]}, {:merkle_patricia_tree, "~> 0.2.6", [hex: :merkle_patricia_tree, repo: "hexpm", optional: false]}, {:poison, "~> 3.1.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 5 | "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 7 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, 8 | "eleveldb": {:hex, :eleveldb, "2.2.20", "1fff63a5055bbf4bf821f797ef76065882b193f5e8095f95fcd9287187773b58", [:rebar3], [], "hexpm"}, 9 | "evm": {:hex, :evm, "0.1.14", "59a3fd332beb6a529cd7c2571a87a7f63c51200f2de381a412ee9bb02f5cbbcc", [:mix], [{:keccakf1600, "~> 2.0.0", [hex: :keccakf1600_orig, repo: "hexpm", optional: false]}, {:merkle_patricia_tree, "~> 0.2.5", [hex: :merkle_patricia_tree, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "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"}, 11 | "ex_rlp": {:hex, :ex_rlp, "0.2.1", "bd320900d6316cdfe01d365d4bda22eb2f39b359798daeeffd3bd1ca7ba958ec", [:mix], [], "hexpm"}, 12 | "exleveldb": {:hex, :exleveldb, "0.11.1", "37c0414208a50d2419d8246305e6c4a33ed339c25c0bdfa27774795e1e089f7b", [:mix], [{:eleveldb, "~> 2.2.19", [hex: :eleveldb, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "exth_crypto": {:hex, :exth_crypto, "0.1.6", "8e636a9bcb75d8e32451be96e547a495121ed2178d078db294edb0f81f7cf2e8", [:mix], [{:binary, "~> 0.0.4", [hex: :binary, repo: "hexpm", optional: false]}, {:keccakf1600, "~> 2.0.0", [hex: :keccakf1600_orig, repo: "hexpm", optional: false]}, {:libsecp256k1, "~> 0.1.9", [hex: :libsecp256k1, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "hex_prefix": {:hex, :hex_prefix, "0.1.0", "e96b5cbb6ad8493196ce193726240023f5ce0ae0753118a19a5b43e2db0267ca", [:mix], [], "hexpm"}, 15 | "inet_ext": {:hex, :inet_ext, "0.4.0", "ef51fe5ea13db6b40cba48e66d9117bbd31e5a4347fa432b83d0c0547c7ab522", [:rebar3], [], "hexpm"}, 16 | "keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], [], "hexpm"}, 17 | "libsecp256k1": {:hex, :libsecp256k1, "0.1.9", "e725f31364cda7b554d56ce2bb976241303dde5ffd1ad59598513297bf1f2af6", [:make, :mix], [{:mix_erlang_tasks, "0.1.0", [hex: :mix_erlang_tasks, repo: "hexpm", optional: false]}], "hexpm"}, 18 | "makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 19 | "makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "merkle_patricia_tree": {:hex, :merkle_patricia_tree, "0.2.6", "4adcc768318582b4ee1cc7ec10b8851fa5708804ead5b7ca9153cec07d57bbdc", [:mix], [{:ex_rlp, "~> 0.2.0", [hex: :ex_rlp, repo: "hexpm", optional: false]}, {:exleveldb, "~> 0.11.1", [hex: :exleveldb, repo: "hexpm", optional: false]}, {:hex_prefix, "~> 0.1.0", [hex: :hex_prefix, repo: "hexpm", optional: false]}, {:keccakf1600, "~> 2.0.0", [hex: :keccakf1600_orig, repo: "hexpm", optional: false]}], "hexpm"}, 21 | "mix_erlang_tasks": {:hex, :mix_erlang_tasks, "0.1.0", "36819fec60b80689eb1380938675af215565a89320a9e29c72c70d97512e4649", [:mix], [], "hexpm"}, 22 | "nat_upnp": {:hex, :nat_upnp, "0.1.0", "959652257a7b6f8ec2bc164d61468e15e5d3945ac57578f3064fcf179379f9b2", [:rebar3], [{:inet_ext, "0.4.0", [hex: :inet_ext, repo: "hexpm", optional: false]}], "hexpm"}, 23 | "nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"}, 24 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/ex_wire/adapters/udp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Adapter.UDPTest do 2 | use ExUnit.Case 3 | doctest ExWire.Adapter.UDP 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.ConfigTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Config 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/crypto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.CryptoTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Crypto 4 | 5 | test "recover public key from signature" do 6 | {signature, _r, _s, recovery_id} = ExthCrypto.Signature.sign_digest("hi mom", ExthCrypto.Test.private_key(:key_a)) 7 | {:ok, public_key} = ExthCrypto.Signature.recover("hi mom", signature, recovery_id) 8 | 9 | assert public_key == ExthCrypto.Test.public_key(:key_a) 10 | end 11 | 12 | end -------------------------------------------------------------------------------- /test/ex_wire/framing/frame_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Framing.FrameTest do 2 | use ExUnit.Case, async: true 3 | alias ExWire.Framing.Frame 4 | alias ExthCrypto.AES 5 | 6 | test "mac encoder" do 7 | mac_secret = "2212767d793a7a3d66f869ae324dd11bd17044b82c9f463b8a541a4d089efec5" |> ExthCrypto.Math.hex_to_bin 8 | input_1 = "12532abaec065082a3cf1da7d0136f15" |> ExthCrypto.Math.hex_to_bin 9 | input_2 = "7e99f682356fdfbc6b67a9562787b18a" |> ExthCrypto.Math.hex_to_bin 10 | expected_1 = "89464c6b04e7c99e555c81d3f7266a05" 11 | expected_2 = "85c070030589ef9c7a2879b3a8489316" 12 | 13 | mac_encoder = {ExthCrypto.AES, ExthCrypto.AES.block_size, :ecb} 14 | 15 | assert expected_1 == ExthCrypto.Cipher.encrypt(input_1, mac_secret, mac_encoder) |> Binary.take(-16) |> ExthCrypto.Math.bin_to_hex 16 | assert expected_2 == ExthCrypto.Cipher.encrypt(input_2, mac_secret, mac_encoder) |> Binary.take(-16) |> ExthCrypto.Math.bin_to_hex 17 | end 18 | 19 | test "simple frame test read / write" do 20 | hash = <<1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1>> 21 | symmetric_key = ExthCrypto.Hash.Keccak.kec(<<>>) 22 | mac_secret = ExthCrypto.Hash.Keccak.kec(<<>>) 23 | ingress_mac = ExthCrypto.MAC.init(:fake, [hash]) 24 | egress_mac = ExthCrypto.MAC.init(:fake, [hash]) 25 | 26 | secrets = %ExWire.Framing.Secrets{ 27 | egress_mac: ingress_mac, 28 | ingress_mac: egress_mac, 29 | mac_encoder: {AES, AES.block_size, :ecb}, 30 | mac_secret: mac_secret, 31 | encoder_stream: AES.stream_init(:ctr, symmetric_key, <<0::size(128)>>), 32 | decoder_stream: AES.stream_init(:ctr, symmetric_key, <<0::size(128)>>) 33 | } 34 | 35 | {frame, _updated_secrets} = Frame.frame(8, [1, 2, 3, 4], secrets) 36 | 37 | assert frame |> ExthCrypto.Math.bin_to_hex == "00828ddae471818bb0bfa6b551d1cb4201010101010101010101010101010101ba628a4ba590cb43f7848f41c438288501010101010101010101010101010101" 38 | 39 | {:ok, packet_type, packet_data, frame_rest, _secrets} = Frame.unframe(frame <> "hello", secrets) 40 | 41 | assert frame_rest == "hello" 42 | assert packet_type == 8 43 | assert packet_data == [<<1>>, <<2>>, <<3>>, <<4>>] 44 | end 45 | 46 | end -------------------------------------------------------------------------------- /test/ex_wire/framing/secrets_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Framing.SecretsTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Framing.Secrets 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/handler/find_neighbours_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler.FindNeighboursTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handler.FindNeighbours 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/handler/ping_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handler.PingTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handler.Ping 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/handler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.HandlerTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handler 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/handshake/eip_8_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake.EIP8Test do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handshake.EIP8 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/handshake/handshake_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HandshakeTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handshake 4 | alias ExWire.Handshake 5 | 6 | test "handshake build and handle auth msg / ack resp via eip-8" do 7 | my_static_public_key = ExthCrypto.Test.public_key(:key_a) 8 | my_static_private_key = ExthCrypto.Test.private_key(:key_a) 9 | her_static_public_key = ExthCrypto.Test.public_key(:key_b) 10 | 11 | {my_auth_msg, my_ephemeral_key_pair, _nonce} = Handshake.build_auth_msg( 12 | my_static_public_key, 13 | my_static_private_key, 14 | her_static_public_key 15 | ) 16 | 17 | {:ok, encoded_auth_msg} = my_auth_msg 18 | |> Handshake.Struct.AuthMsgV4.serialize() 19 | |> Handshake.EIP8.wrap_eip_8(her_static_public_key, "1.2.3.4", my_ephemeral_key_pair) 20 | 21 | {:ok, her_auth_msg, <<>>} = Handshake.read_auth_msg(encoded_auth_msg, ExthCrypto.Test.private_key(:key_b), "1.2.3.4") 22 | 23 | # Same auth message, except we've added the remote ephemeral public key 24 | assert her_auth_msg.remote_ephemeral_public_key != nil 25 | assert my_auth_msg == %{her_auth_msg | remote_ephemeral_public_key: nil} 26 | 27 | my_ack_resp = Handshake.build_ack_resp(her_auth_msg.remote_ephemeral_public_key) 28 | 29 | {:ok, encoded_ack_msg} = my_ack_resp 30 | |> Handshake.Struct.AckRespV4.serialize() 31 | |> Handshake.EIP8.wrap_eip_8(her_static_public_key, "1.2.3.4", my_ephemeral_key_pair) 32 | 33 | {:ok, her_ack_resp, _ack_bin, <<>>} = Handshake.read_ack_resp(encoded_ack_msg, ExthCrypto.Test.private_key(:key_b), "1.2.3.4") 34 | 35 | assert my_ack_resp == her_ack_resp 36 | end 37 | 38 | test "handshake auth plain" do 39 | secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" |> ExthCrypto.Math.hex_to_bin 40 | auth = """ 41 | 048ca79ad18e4b0659fab4853fe5bc58eb83992980f4c9cc147d2aa31532efd29a3d3dc6a3d89eaf 42 | 913150cfc777ce0ce4af2758bf4810235f6e6ceccfee1acc6b22c005e9e3a49d6448610a58e98744 43 | ba3ac0399e82692d67c1f58849050b3024e21a52c9d3b01d871ff5f210817912773e610443a9ef14 44 | 2e91cdba0bd77b5fdf0769b05671fc35f83d83e4d3b0b000c6b2a1b1bba89e0fc51bf4e460df3105 45 | c444f14be226458940d6061c296350937ffd5e3acaceeaaefd3c6f74be8e23e0f45163cc7ebd7622 46 | 0f0128410fd05250273156d548a414444ae2f7dea4dfca2d43c057adb701a715bf59f6fb66b2d1d2 47 | 0f2c703f851cbf5ac47396d9ca65b6260bd141ac4d53e2de585a73d1750780db4c9ee4cd4d225173 48 | a4592ee77e2bd94d0be3691f3b406f9bba9b591fc63facc016bfa8 49 | """ |> String.replace("\n", "") |> ExthCrypto.Math.hex_to_bin 50 | 51 | {:ok, auth_msg, <<>>} = Handshake.read_auth_msg(auth, secret, "1.2.3.4") 52 | 53 | assert auth_msg.remote_public_key == "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877" |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 54 | assert auth_msg.remote_nonce == "7e968bba13b6c50e2c4cd7f241cc0d64d1ac25c7f5952df231ac6a2bda8ee5d6" |> ExthCrypto.Math.hex_to_bin 55 | assert auth_msg.remote_ephemeral_public_key == "654d1044b69c577a44e5f01a1209523adb4026e70c62d1c13a067acabc09d2667a49821a0ad4b634554d330a15a58fe61f8a8e0544b310c6de7b0c8da7528a8d" |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 56 | assert auth_msg.remote_version == 63 57 | end 58 | 59 | test "handshake auth eip 8" do 60 | secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" |> ExthCrypto.Math.hex_to_bin 61 | auth = """ 62 | 01b304ab7578555167be8154d5cc456f567d5ba302662433674222360f08d5f1534499d3678b513b 63 | 0fca474f3a514b18e75683032eb63fccb16c156dc6eb2c0b1593f0d84ac74f6e475f1b8d56116b84 64 | 9634a8c458705bf83a626ea0384d4d7341aae591fae42ce6bd5c850bfe0b999a694a49bbbaf3ef6c 65 | da61110601d3b4c02ab6c30437257a6e0117792631a4b47c1d52fc0f8f89caadeb7d02770bf999cc 66 | 147d2df3b62e1ffb2c9d8c125a3984865356266bca11ce7d3a688663a51d82defaa8aad69da39ab6 67 | d5470e81ec5f2a7a47fb865ff7cca21516f9299a07b1bc63ba56c7a1a892112841ca44b6e0034dee 68 | 70c9adabc15d76a54f443593fafdc3b27af8059703f88928e199cb122362a4b35f62386da7caad09 69 | c001edaeb5f8a06d2b26fb6cb93c52a9fca51853b68193916982358fe1e5369e249875bb8d0d0ec3 70 | 6f917bc5e1eafd5896d46bd61ff23f1a863a8a8dcd54c7b109b771c8e61ec9c8908c733c0263440e 71 | 2aa067241aaa433f0bb053c7b31a838504b148f570c0ad62837129e547678c5190341e4f1693956c 72 | 3bf7678318e2d5b5340c9e488eefea198576344afbdf66db5f51204a6961a63ce072c8926c 73 | """ |> String.replace("\n", "") |> ExthCrypto.Math.hex_to_bin 74 | 75 | {:ok, auth_msg, <<>>} = Handshake.read_auth_msg(auth, secret, "1.2.3.4") 76 | 77 | assert auth_msg.remote_public_key == "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877" |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 78 | assert auth_msg.remote_nonce == "7e968bba13b6c50e2c4cd7f241cc0d64d1ac25c7f5952df231ac6a2bda8ee5d6" |> ExthCrypto.Math.hex_to_bin 79 | assert auth_msg.remote_ephemeral_public_key == "654d1044b69c577a44e5f01a1209523adb4026e70c62d1c13a067acabc09d2667a49821a0ad4b634554d330a15a58fe61f8a8e0544b310c6de7b0c8da7528a8d" |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 80 | assert auth_msg.remote_version == 4 81 | end 82 | 83 | test "handshake auth eip 8 - 2" do 84 | secret = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" |> ExthCrypto.Math.hex_to_bin 85 | auth = """ 86 | 01b8044c6c312173685d1edd268aa95e1d495474c6959bcdd10067ba4c9013df9e40ff45f5bfd6f7 87 | 2471f93a91b493f8e00abc4b80f682973de715d77ba3a005a242eb859f9a211d93a347fa64b597bf 88 | 280a6b88e26299cf263b01b8dfdb712278464fd1c25840b995e84d367d743f66c0e54a586725b7bb 89 | f12acca27170ae3283c1073adda4b6d79f27656993aefccf16e0d0409fe07db2dc398a1b7e8ee93b 90 | cd181485fd332f381d6a050fba4c7641a5112ac1b0b61168d20f01b479e19adf7fdbfa0905f63352 91 | bfc7e23cf3357657455119d879c78d3cf8c8c06375f3f7d4861aa02a122467e069acaf513025ff19 92 | 6641f6d2810ce493f51bee9c966b15c5043505350392b57645385a18c78f14669cc4d960446c1757 93 | 1b7c5d725021babbcd786957f3d17089c084907bda22c2b2675b4378b114c601d858802a55345a15 94 | 116bc61da4193996187ed70d16730e9ae6b3bb8787ebcaea1871d850997ddc08b4f4ea668fbf3740 95 | 7ac044b55be0908ecb94d4ed172ece66fd31bfdadf2b97a8bc690163ee11f5b575a4b44e36e2bfb2 96 | f0fce91676fd64c7773bac6a003f481fddd0bae0a1f31aa27504e2a533af4cef3b623f4791b2cca6 97 | d490 98 | """ |> String.replace("\n", "") |> ExthCrypto.Math.hex_to_bin 99 | 100 | {:ok, auth_msg, <<>>} = Handshake.read_auth_msg(auth, secret, "1.2.3.4") 101 | 102 | assert auth_msg.remote_public_key == "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877" |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 103 | assert auth_msg.remote_nonce == "7e968bba13b6c50e2c4cd7f241cc0d64d1ac25c7f5952df231ac6a2bda8ee5d6" |> ExthCrypto.Math.hex_to_bin 104 | assert auth_msg.remote_ephemeral_public_key == "654d1044b69c577a44e5f01a1209523adb4026e70c62d1c13a067acabc09d2667a49821a0ad4b634554d330a15a58fe61f8a8e0544b310c6de7b0c8da7528a8d" |> ExthCrypto.Math.hex_to_bin |> ExthCrypto.Key.raw_to_der 105 | assert auth_msg.remote_version == 56 106 | end 107 | 108 | test "handshake ack plain" do 109 | _remote_public_key = "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877" |> ExthCrypto.Math.hex_to_bin 110 | secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee" |> ExthCrypto.Math.hex_to_bin 111 | ack = """ 112 | 049f8abcfa9c0dc65b982e98af921bc0ba6e4243169348a236abe9df5f93aa69d99cadddaa387662 113 | b0ff2c08e9006d5a11a278b1b3331e5aaabf0a32f01281b6f4ede0e09a2d5f585b26513cb794d963 114 | 5a57563921c04a9090b4f14ee42be1a5461049af4ea7a7f49bf4c97a352d39c8d02ee4acc416388c 115 | 1c66cec761d2bc1c72da6ba143477f049c9d2dde846c252c111b904f630ac98e51609b3b1f58168d 116 | dca6505b7196532e5f85b259a20c45e1979491683fee108e9660edbf38f3add489ae73e3dda2c71b 117 | d1497113d5c755e942d1 118 | """ |> String.replace("\n", "") |> ExthCrypto.Math.hex_to_bin 119 | 120 | {:ok, ack_resp, _ack_resp_bin, <<>>} = Handshake.read_ack_resp(ack, secret, "1.2.3.4") 121 | 122 | assert ack_resp.remote_nonce == "559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd" |> ExthCrypto.Math.hex_to_bin 123 | assert ack_resp.remote_ephemeral_public_key == "b6d82fa3409da933dbf9cb0140c5dde89f4e64aec88d476af648880f4a10e1e49fe35ef3e69e93dd300b4797765a747c6384a6ecf5db9c2690398607a86181e4" |> ExthCrypto.Math.hex_to_bin 124 | assert ack_resp.remote_version == 63 125 | end 126 | 127 | test "handshake ack eip 8" do 128 | _remote_public_key = "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877" |> ExthCrypto.Math.hex_to_bin 129 | secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee" |> ExthCrypto.Math.hex_to_bin 130 | ack = """ 131 | 01ea0451958701280a56482929d3b0757da8f7fbe5286784beead59d95089c217c9b917788989470 132 | b0e330cc6e4fb383c0340ed85fab836ec9fb8a49672712aeabbdfd1e837c1ff4cace34311cd7f4de 133 | 05d59279e3524ab26ef753a0095637ac88f2b499b9914b5f64e143eae548a1066e14cd2f4bd7f814 134 | c4652f11b254f8a2d0191e2f5546fae6055694aed14d906df79ad3b407d94692694e259191cde171 135 | ad542fc588fa2b7333313d82a9f887332f1dfc36cea03f831cb9a23fea05b33deb999e85489e645f 136 | 6aab1872475d488d7bd6c7c120caf28dbfc5d6833888155ed69d34dbdc39c1f299be1057810f34fb 137 | e754d021bfca14dc989753d61c413d261934e1a9c67ee060a25eefb54e81a4d14baff922180c395d 138 | 3f998d70f46f6b58306f969627ae364497e73fc27f6d17ae45a413d322cb8814276be6ddd13b885b 139 | 201b943213656cde498fa0e9ddc8e0b8f8a53824fbd82254f3e2c17e8eaea009c38b4aa0a3f306e8 140 | 797db43c25d68e86f262e564086f59a2fc60511c42abfb3057c247a8a8fe4fb3ccbadde17514b7ac 141 | 8000cdb6a912778426260c47f38919a91f25f4b5ffb455d6aaaf150f7e5529c100ce62d6d92826a7 142 | 1778d809bdf60232ae21ce8a437eca8223f45ac37f6487452ce626f549b3b5fdee26afd2072e4bc7 143 | 5833c2464c805246155289f4 144 | """ |> String.replace("\n", "") |> ExthCrypto.Math.hex_to_bin 145 | 146 | {:ok, ack_resp, _ack_resp_bin, <<>>} = Handshake.read_ack_resp(ack, secret, "1.2.3.4") 147 | 148 | assert ack_resp.remote_nonce == "559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd" |> ExthCrypto.Math.hex_to_bin 149 | assert ack_resp.remote_ephemeral_public_key == "b6d82fa3409da933dbf9cb0140c5dde89f4e64aec88d476af648880f4a10e1e49fe35ef3e69e93dd300b4797765a747c6384a6ecf5db9c2690398607a86181e4" |> ExthCrypto.Math.hex_to_bin 150 | assert ack_resp.remote_version == 4 151 | end 152 | 153 | test "handshake ack eip 8 - 2" do 154 | _remote_public_key = "fda1cff674c90c9a197539fe3dfb53086ace64f83ed7c6eabec741f7f381cc803e52ab2cd55d5569bce4347107a310dfd5f88a010cd2ffd1005ca406f1842877" |> ExthCrypto.Math.hex_to_bin 155 | secret = "49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee" |> ExthCrypto.Math.hex_to_bin 156 | ack = """ 157 | 01f004076e58aae772bb101ab1a8e64e01ee96e64857ce82b1113817c6cdd52c09d26f7b90981cd7 158 | ae835aeac72e1573b8a0225dd56d157a010846d888dac7464baf53f2ad4e3d584531fa203658fab0 159 | 3a06c9fd5e35737e417bc28c1cbf5e5dfc666de7090f69c3b29754725f84f75382891c561040ea1d 160 | dc0d8f381ed1b9d0d4ad2a0ec021421d847820d6fa0ba66eaf58175f1b235e851c7e2124069fbc20 161 | 2888ddb3ac4d56bcbd1b9b7eab59e78f2e2d400905050f4a92dec1c4bdf797b3fc9b2f8e84a482f3 162 | d800386186712dae00d5c386ec9387a5e9c9a1aca5a573ca91082c7d68421f388e79127a5177d4f8 163 | 590237364fd348c9611fa39f78dcdceee3f390f07991b7b47e1daa3ebcb6ccc9607811cb17ce51f1 164 | c8c2c5098dbdd28fca547b3f58c01a424ac05f869f49c6a34672ea2cbbc558428aa1fe48bbfd6115 165 | 8b1b735a65d99f21e70dbc020bfdface9f724a0d1fb5895db971cc81aa7608baa0920abb0a565c9c 166 | 436e2fd13323428296c86385f2384e408a31e104670df0791d93e743a3a5194ee6b076fb6323ca59 167 | 3011b7348c16cf58f66b9633906ba54a2ee803187344b394f75dd2e663a57b956cb830dd7a908d4f 168 | 39a2336a61ef9fda549180d4ccde21514d117b6c6fd07a9102b5efe710a32af4eeacae2cb3b1dec0 169 | 35b9593b48b9d3ca4c13d245d5f04169b0b1 170 | """ |> String.replace("\n", "") |> ExthCrypto.Math.hex_to_bin 171 | 172 | {:ok, ack_resp, _ack_resp_bin, <<>>} = Handshake.read_ack_resp(ack, secret, "1.2.3.4") 173 | 174 | assert ack_resp.remote_nonce == "559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd" |> ExthCrypto.Math.hex_to_bin 175 | assert ack_resp.remote_ephemeral_public_key == "b6d82fa3409da933dbf9cb0140c5dde89f4e64aec88d476af648880f4a10e1e49fe35ef3e69e93dd300b4797765a747c6384a6ecf5db9c2690398607a86181e4" |> ExthCrypto.Math.hex_to_bin 176 | assert ack_resp.remote_version == 57 177 | end 178 | 179 | end -------------------------------------------------------------------------------- /test/ex_wire/handshake/struct/ack_resp_v4_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake.Struct.AckRespV4Test do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handshake.Struct.AckRespV4 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/handshake/struct/auth_msg_v4_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Handshake.Struct.AuthMsgV4Test do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Handshake.Struct.AuthMsgV4 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/message/find_neighbours_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.FindNeighboursTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Message.FindNeighbours 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/message/neighbours_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.NeighboursTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Message.Neighbours 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/message/ping_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.PingTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Message.Ping 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/message/pong_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Message.PongTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Message.Pong 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/message_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.MessageTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Message 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/network_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.NetworkTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Network 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/block_bodies_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.BlockBodiesTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.BlockBodies 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/block_headers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.BlockHeadersTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.BlockHeaders 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/disconnect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.DisconnectTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.Disconnect 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/get_block_bodies_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.GetBlockBodiesTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.GetBlockBodies 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/get_block_headers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.GetBlockHeadersTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.GetBlockHeaders 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/hello_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.HelloTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.Hello 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/new_block_hashes_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.NewBlockHashesTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.NewBlockHashes 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/new_block_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.NewBlockTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.NewBlock 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/ping_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.PingTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.Ping 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/pong_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.PongTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.Pong 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/status_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.StatusTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.Status 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet/transactions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Packet.TransactionsTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet.Transactions 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/packet_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.PacketTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Packet 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/peer_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.PeerSupervisorTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.PeerSupervisor 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.ProtocolTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Protocol 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/struct/block_queue_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.BlockQueueTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Struct.BlockQueue 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/struct/block_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.BlockTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Struct.Block 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/struct/endpoint_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.EndpointTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Struct.Endpoint 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/struct/neighbour_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.NeighbourTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Struct.Neighbour 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/struct/peer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Struct.PeerTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Struct.Peer 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/sync_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.SyncTest do 2 | use ExUnit.Case, async: true 3 | doctest ExWire.Sync 4 | 5 | end -------------------------------------------------------------------------------- /test/ex_wire/util/timestamp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Util.TimestampTest do 2 | use ExUnit.Case, async: true 3 | alias ExWire.Util.Timestamp 4 | 5 | test "returns a valid timestamp" do 6 | assert Timestamp.now > 0 7 | end 8 | end -------------------------------------------------------------------------------- /test/ex_wire_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWireTest do 2 | use ExUnit.Case 3 | doctest ExWire 4 | 5 | alias ExWire.Protocol 6 | alias ExWire.Message.Ping 7 | alias ExWire.Message.Pong 8 | alias ExWire.Message.Neighbours 9 | alias ExWire.Message.FindNeighbours 10 | alias ExWire.Util.Timestamp 11 | 12 | @them %ExWire.Struct.Endpoint{ 13 | ip: {0, 0, 0, 1}, 14 | udp_port: 30303, 15 | tcp_port: nil, 16 | } 17 | 18 | @us %ExWire.Struct.Endpoint{ 19 | ip: {0, 0, 0, 2}, 20 | udp_port: 30303, 21 | tcp_port: nil, 22 | } 23 | 24 | setup do 25 | Process.register self(), :test 26 | 27 | :ok 28 | end 29 | 30 | test "`ping` responds with a `pong`" do 31 | timestamp = Timestamp.now 32 | 33 | ping = %Ping{ 34 | version: 4, 35 | to: @them, 36 | from: @us, 37 | timestamp: timestamp 38 | } 39 | 40 | hash = fake_send(ping, timestamp + 1) 41 | 42 | assert_receive_message(%Pong{ 43 | to: @us, 44 | hash: hash, 45 | timestamp: timestamp + 1 46 | }) 47 | end 48 | 49 | test "`find_neighbours` responds with `neighbors`" do 50 | timestamp = Timestamp.now 51 | 52 | find_neighbours = %FindNeighbours{ 53 | target: <<1>>, 54 | timestamp: timestamp 55 | } 56 | 57 | fake_send(find_neighbours, timestamp + 1) 58 | 59 | assert_receive_message(%Neighbours{ 60 | nodes: [], 61 | timestamp: timestamp + 1 62 | }) 63 | end 64 | 65 | def assert_receive_message(message) do 66 | message = message |> Protocol.encode(ExWire.Config.private_key()) 67 | assert_receive(%{data: ^message, to: @us}) 68 | end 69 | 70 | def fake_send(message, timestamp) do 71 | encoded_message = Protocol.encode(message, ExWire.Config.private_key()) 72 | 73 | GenServer.cast( 74 | :test_network_adapter, 75 | { 76 | :fake_recieve, 77 | %{ 78 | data: encoded_message, 79 | remote_host: @us, 80 | timestamp: timestamp, 81 | } 82 | } 83 | ) 84 | 85 | <> = encoded_message 86 | 87 | hash 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/integration/remote_connection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExWire.RemoteConnectionTest do 2 | @moduledoc """ 3 | This test case will connect to a live running node (preferably Geth or Parity). 4 | We'll attempt to pull blocks from the remote peer. 5 | 6 | Before starting, you may want to run a Parity or Geth node. 7 | 8 | E.g. `cargo run -- --chain=ropsten --bootnodes=` 9 | E.g. `cargo run -- --chain=ropsten --bootnodes= --logging network,discovery=trace` 10 | E.g. `build/bin/geth --testnet --bootnodes= --port 31313 --verbosity 6` 11 | 12 | If you do set, set the `REMOTE_TEST_PEER` environment variable to the full `enode://...` address. 13 | """ 14 | use ExUnit.Case, async: true 15 | 16 | require Logger 17 | 18 | alias ExWire.Packet 19 | alias ExWire.Adapter.TCP 20 | 21 | @moduletag integration: true 22 | @moduletag network: true 23 | 24 | @local_peer {127, 0, 0, 1} 25 | @local_peer_port 35353 26 | @local_tcp_port 36363 27 | 28 | def receive(inbound_message, pid) do 29 | send(pid, {:inbound_message, inbound_message}) 30 | end 31 | 32 | def receive_packet(inbound_packet, pid) do 33 | send(pid, {:incoming_packet, inbound_packet}) 34 | end 35 | 36 | @remote_test_peer System.get_env("REMOTE_TEST_PEER") || ( ExWire.Config.chain.nodes |> List.last ) 37 | 38 | test "connect to remote peer for discovery" do 39 | %URI{ 40 | scheme: "enode", 41 | userinfo: remote_id, 42 | host: remote_host, 43 | port: remote_peer_port 44 | } = URI.parse(@remote_test_peer) 45 | 46 | {:ok, remote_ip} = :inet.ip(remote_host |> String.to_charlist) 47 | 48 | remote_peer = %ExWire.Struct.Endpoint{ 49 | ip: remote_ip, 50 | udp_port: remote_peer_port, 51 | } 52 | 53 | # First, start a new client 54 | {:ok, client_pid} = ExWire.Adapter.UDP.start_link({__MODULE__, [self()]}, @local_peer_port, __MODULE__.Test) 55 | 56 | # Now, we'll send a ping / pong to verify connectivity 57 | timestamp = ExWire.Util.Timestamp.soon() 58 | 59 | ping = %ExWire.Message.Ping{ 60 | version: 1, 61 | from: %ExWire.Struct.Endpoint{ip: @local_peer, tcp_port: @local_tcp_port, udp_port: @local_peer_port}, 62 | to: %ExWire.Struct.Endpoint{ip: remote_ip, tcp_port: nil, udp_port: remote_peer_port}, 63 | timestamp: timestamp, 64 | } 65 | 66 | ExWire.Network.send(ping, client_pid, remote_peer) 67 | 68 | receive_pong(timestamp, client_pid, remote_peer, remote_id) 69 | end 70 | 71 | def receive_pong(timestamp, client_pid, remote_peer, remote_id) do 72 | receive do 73 | {:inbound_message, inbound_message} -> 74 | # Check the message looks good 75 | message = decode_message(inbound_message) 76 | 77 | assert message.__struct__ == ExWire.Message.Pong 78 | assert %ExWire.Struct.Endpoint{} = message.to 79 | assert message.timestamp >= timestamp 80 | 81 | # If so, we're going to continue on to "find neighbours." 82 | find_neighbours = %ExWire.Message.FindNeighbours{ 83 | target: remote_id |> ExthCrypto.Math.hex_to_bin, 84 | timestamp: ExWire.Util.Timestamp.soon() 85 | } 86 | 87 | ExWire.Network.send(find_neighbours, client_pid, remote_peer) 88 | 89 | receive_neighbours() 90 | after 2_000 -> 91 | raise "Expected pong, but did not receive before timeout." 92 | end 93 | end 94 | 95 | def receive_neighbours() do 96 | receive do 97 | {:inbound_message, inbound_message} -> 98 | # Check the message looks good 99 | message = decode_message(inbound_message) 100 | 101 | assert Enum.count(message.nodes) > 5 102 | after 2_000 -> 103 | raise "Expected neighbours, but did not receive before timeout." 104 | end 105 | end 106 | 107 | def decode_message(%ExWire.Network.InboundMessage{ 108 | data: << 109 | _hash :: size(256), 110 | _signature :: size(512), 111 | _recovery_id:: integer-size(8), 112 | type:: integer-size(8), 113 | data :: bitstring 114 | >> 115 | }) do 116 | ExWire.Message.decode(type, data) 117 | end 118 | 119 | test "connect to remote peer for handshake" do 120 | {:ok, peer} = ExWire.Struct.Peer.from_uri(@remote_test_peer) 121 | 122 | {:ok, client_pid} = TCP.start_link(:outbound, peer) 123 | 124 | TCP.subscribe(client_pid, {__MODULE__, :receive_packet, [self()]}) 125 | 126 | receive_status(client_pid) 127 | end 128 | 129 | def receive_status(client_pid) do 130 | receive do 131 | {:incoming_packet, _packet=%Packet.Status{best_hash: _best_hash, total_difficulty: total_difficulty, genesis_hash: genesis_hash}} -> 132 | # Send a simple status message 133 | TCP.send_packet(client_pid, %Packet.Status{ 134 | protocol_version: ExWire.Config.protocol_version(), 135 | network_id: ExWire.Config.network_id(), 136 | total_difficulty: total_difficulty, 137 | best_hash: genesis_hash, 138 | genesis_hash: genesis_hash 139 | }) 140 | 141 | ExWire.Adapter.TCP.send_packet(client_pid, %ExWire.Packet.GetBlockHeaders{ 142 | block_identifier: genesis_hash, 143 | max_headers: 1, 144 | skip: 0, 145 | reverse: false 146 | }) 147 | 148 | receive_block_headers(client_pid) 149 | {:incoming_packet, packet} -> 150 | if System.get_env("TRACE"), do: Logger.debug("Expecting status packet, got: #{inspect packet}") 151 | 152 | receive_status(client_pid) 153 | after 3_000 -> 154 | raise "Expected status, but did not receive before timeout." 155 | end 156 | end 157 | 158 | def receive_block_headers(client_pid) do 159 | receive do 160 | {:incoming_packet, _packet=%Packet.BlockHeaders{headers: [header]}} -> 161 | ExWire.Adapter.TCP.send_packet(client_pid, %ExWire.Packet.GetBlockBodies{hashes: [header |> Block.Header.hash]}) 162 | 163 | receive_block_bodies(client_pid) 164 | {:incoming_packet, packet} -> 165 | if System.get_env("TRACE"), do: Logger.debug("Expecting block headers packet, got: #{inspect packet}") 166 | 167 | receive_block_headers(client_pid) 168 | after 3_000 -> 169 | raise "Expected block headers, but did not receive before timeout." 170 | end 171 | end 172 | 173 | def receive_block_bodies(client_pid) do 174 | receive do 175 | {:incoming_packet, _packet=%Packet.BlockBodies{blocks: [block]}} -> 176 | # This is a genesis block 177 | assert block.transactions_list == [] 178 | assert block.ommers == [] 179 | 180 | Logger.warn("Successfully received genesis block from peer.") 181 | {:incoming_packet, packet} -> 182 | if System.get_env("TRACE"), do: Logger.debug("Expecting block bodies packet, got: #{inspect packet}") 183 | 184 | receive_block_bodies(client_pid) 185 | after 3_000 -> 186 | raise "Expected block bodies, but did not receive before timeout." 187 | end 188 | end 189 | end -------------------------------------------------------------------------------- /test/integration/wire_to_wire_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WireToWireTest do 2 | @moduledoc """ 3 | This test starts a server and connects a peer to it. It 4 | checks that a PING / PONG can be successfully communicated. 5 | """ 6 | 7 | use ExUnit.Case, async: true 8 | 9 | @moduletag integration: true 10 | @localhost {127, 0, 0, 1} 11 | @us_port 8888 12 | @them_port 9999 13 | 14 | setup do 15 | server = ExWire.start(nil, [network_adapter: ExWire.Adapter.UDP, port: @them_port, name: __MODULE__.Server]) 16 | 17 | remote_host = %ExWire.Struct.Endpoint{ 18 | ip: @localhost, 19 | udp_port: @them_port, 20 | } 21 | 22 | {:ok, %{server: server, remote_host: remote_host}} 23 | end 24 | 25 | def receive(inbound_message, pid) do 26 | send(pid, {:inbound_message, inbound_message}) 27 | end 28 | 29 | test "ping / pong", %{remote_host: remote_host} do 30 | {:ok, client_pid} = ExWire.Adapter.UDP.start_link({__MODULE__, [self()]}, @us_port, __MODULE__.Test) 31 | 32 | timestamp = ExWire.Util.Timestamp.now() 33 | 34 | ping = %ExWire.Message.Ping{ 35 | version: 1, 36 | from: %ExWire.Struct.Endpoint{ip: @localhost, tcp_port: nil, udp_port: @us_port}, 37 | to: %ExWire.Struct.Endpoint{ip: @localhost, tcp_port: nil, udp_port: @them_port}, 38 | timestamp: timestamp, 39 | } 40 | 41 | ExWire.Network.send(ping, client_pid, remote_host) 42 | 43 | receive do 44 | {:inbound_message, inbound_message} -> 45 | message = decode_message(inbound_message) 46 | 47 | assert message.__struct__ == ExWire.Message.Pong 48 | assert message.to == %ExWire.Struct.Endpoint{ip: {127, 0, 0, 1}, tcp_port: nil, udp_port: @us_port} 49 | assert message.timestamp >= timestamp 50 | after 2_000 -> 51 | raise "Expected pong, but did not receive before timeout." 52 | end 53 | end 54 | 55 | def decode_message(%ExWire.Network.InboundMessage{ 56 | data: << 57 | _hash :: size(256), 58 | _signature :: size(512), 59 | _recovery_id:: integer-size(8), 60 | type:: integer-size(8), 61 | data :: bitstring 62 | >> 63 | }) do 64 | ExWire.Message.decode(type, data) 65 | end 66 | end -------------------------------------------------------------------------------- /test/support/ex_wire/adapter/test.ex: -------------------------------------------------------------------------------- 1 | defmodule ExWire.Adapter.Test do 2 | use GenServer 3 | 4 | def start_link({network, network_args}, port) do 5 | GenServer.start_link(__MODULE__, %{network: network, network_args: network_args, port: port}) 6 | end 7 | 8 | def init(state) do 9 | Process.register self(), :test_network_adapter 10 | {:ok, state} 11 | end 12 | 13 | def handle_cast({:listen, callback}, state) do 14 | state = Map.put(state, :callback, callback) 15 | {:noreply, state} 16 | end 17 | 18 | def handle_cast({:send, data}, state) do 19 | send :test, data 20 | {:noreply, state} 21 | end 22 | 23 | def handle_cast({:fake_recieve, %{ 24 | data: data, 25 | remote_host: remote_host, 26 | timestamp: timestamp, 27 | }}, 28 | state = %{network: network}) do 29 | network.receive(%ExWire.Network.InboundMessage{ 30 | data: data, 31 | server_pid: self(), 32 | remote_host: remote_host, 33 | timestamp: timestamp, 34 | }, nil) 35 | 36 | {:noreply, state} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------