├── .gitignore ├── .tool-versions ├── LICENCE ├── README.md ├── config └── config.exs ├── lib ├── bitcoin.ex └── bitcoin │ ├── models │ └── peer.ex │ ├── node.ex │ ├── node │ ├── peers.ex │ └── peers │ │ ├── connection_pool.ex │ │ └── discovery.ex │ ├── protocol.ex │ └── protocol │ ├── message.ex │ ├── messages │ ├── addr.ex │ ├── alert.ex │ ├── block.ex │ ├── get_addr.ex │ ├── get_blocks.ex │ ├── get_data.ex │ ├── get_headers.ex │ ├── headers.ex │ ├── inv.ex │ ├── not_found.ex │ ├── ping.ex │ ├── pong.ex │ ├── reject.ex │ ├── tx.ex │ ├── verack.ex │ └── version.ex │ └── types │ ├── block_header.ex │ ├── integer.ex │ ├── integer_array.ex │ ├── inventory_vector.ex │ ├── network_address.ex │ ├── outpoint.ex │ ├── string.ex │ ├── string_array.ex │ ├── transaction_input.ex │ └── transaction_output.ex ├── mix.exs ├── mix.lock └── test ├── data └── blk_100000.dat ├── node └── peer_test.exs ├── protocol ├── message_test.exs ├── messages │ ├── addr_test.exs │ ├── alert_test.exs │ ├── block_test.exs │ ├── get_blocks_test.exs │ ├── get_data_test.exs │ ├── get_headers_test.exs │ ├── headers_test.exs │ ├── inv_test.exs │ ├── not_found_test.exs │ ├── ping_test.exs │ ├── pong_test.exs │ ├── reject_test.exs │ ├── tx_test.exs │ └── version_test.exs └── types │ ├── block_header_test.exs │ ├── integer_array_test.exs │ ├── integer_test.exs │ ├── inventory_vector_test.exs │ ├── network_address_test.exs │ ├── string_array_test.exs │ └── string_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /log 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.4.2 2 | erlang 19.3 3 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Justin Lynn 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin-Ex (Bitcoin Elixir) 2 | 3 | A Bitcoin protocol parser library and full node implementation written in Elixir. 4 | 5 | Current Status: Not even beta quality. Unstable Interfaces. 6 | Not recommended for any use except gross curiosity. 7 | 8 | This is a weekend hack, your improvements and constructive criticisms are highly appreciated. 9 | 10 | ## Licence 11 | See the LICENCE file in the project root. 12 | 13 | ## Contributing 14 | Please fork this repository to your own account, create a feature/{short but descriptive name} branch on your own 15 | repository and submit a pull request back to develop. 16 | 17 | ## Features (Planned) 18 | 19 | * Complete Bitcoin Parsing Library 20 | * Complete Bitcoin Node Implementation 21 | * OTP Clustering/Distribution Support for Non-Stop Operation including Hot Code Reloading w/o Node Restart in Production 22 | * Suitable for Elixir/Erlang Application Embedding (for creating a [Toshi](https://github.com/coinbase/toshi) compatible API or web interface for example) 23 | * Abstract Blockchain Bulk Data and Index Storage Backend Interface (Mnesia, Postgres, etc...) 24 | 25 | ## Feature Status (Current) 26 | 27 | ### It Works (sort of) 28 | 29 | * Protocol Library 30 | * Message Interface 31 | * Message Header Parsing 32 | * Message Type Detection and Parsing 33 | * Common Structure Deserialisation 34 | * varint/varint[] 35 | * varstring/varstring[] 36 | * inventory vector 37 | * network address 38 | * txin/txout/outpoint 39 | * block header 40 | * Message Deserialisation (Individual Messages and their representations) 41 | * addr 42 | * alert 43 | * block 44 | * getaddr 45 | * getblocks 46 | * getdata 47 | * getheaders 48 | * headers 49 | * inv 50 | * notfound 51 | * ping 52 | * pong 53 | * reject 54 | * tx 55 | * version 56 | * OTP Application / Full Node 57 | * Peer 58 | * Connection Pool/Acceptor and Handler 59 | * Discovery 60 | * Strategies 61 | * DNS 62 | 63 | ### Not Yet (In Progress) 64 | 65 | * Protocol Library 66 | * High Level Domain Objects and Actor Representations 67 | * Peer 68 | * Blockchain 69 | * Block 70 | * Transaction Queues 71 | * Event Model 72 | * Logging Strategy 73 | * Common Structure Serialisation 74 | * varint/varint[] 75 | * varstring/varstring[] 76 | * inventory vector 77 | * network address 78 | * txin/txout/outpoint 79 | * block header 80 | * Message Serialisation 81 | * addr 82 | * alert 83 | * block 84 | * getaddr 85 | * getblocks 86 | * getdata 87 | * getheaders 88 | * headers 89 | * inv 90 | * notfound 91 | * ping 92 | * pong 93 | * reject 94 | * tx 95 | * version 96 | * OTP Application / Full Node 97 | * Server Layout and Deployment 98 | * Blockchain Bulk Storage and Index API 99 | * Transaction Script Engine 100 | * Script Parsing (Compression for Serialisation?) 101 | * Virtual Machine for Script Evaluation/Execution (base on GenServer and Callback State System for near-native speed execution of stack machines) 102 | * Peer Connectivity 103 | * Message Exchange / Configuration 104 | 105 | ### Compliance Tests Not Yet Passing but for which Compliance is a Goal 106 | 107 | * TheBlueMatt's [regression test suite](https://github.com/TheBlueMatt/test-scripts) 108 | -------------------------------------------------------------------------------- /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 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/bitcoin.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin do 2 | end 3 | -------------------------------------------------------------------------------- /lib/bitcoin/models/peer.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Models.Peer do 2 | 3 | defstruct ip_address: {0, 0, 0, 0} # IPV4 or IPV6 4 | 5 | @type t :: %Bitcoin.Models.Peer{ 6 | ip_address: tuple 7 | } 8 | 9 | end -------------------------------------------------------------------------------- /lib/bitcoin/node.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Node do 2 | use Application 3 | 4 | defmodule Subsystems do 5 | use Supervisor 6 | 7 | def start_link do 8 | Supervisor.start_link(__MODULE__, :ok, []) 9 | end 10 | 11 | @peer_subsystem_name Bitcoin.Node.Peers 12 | 13 | def init(:ok) do 14 | children = [ 15 | supervisor(@peer_subsystem_name, [[name: @peer_subsystem_name]]) 16 | ] 17 | 18 | supervise([], strategy: :one_for_one) 19 | end 20 | 21 | end 22 | 23 | def start(_type, _args) do 24 | Subsystems.start_link() 25 | end 26 | end -------------------------------------------------------------------------------- /lib/bitcoin/node/peers.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Node.Peers do 2 | use Supervisor 3 | 4 | @discovery_service Bitcoin.Node.Peers.Discovery 5 | @peer_connection_pool_service Bitcoin.Node.Peers.ConnectionPool 6 | 7 | def start_link(_opts) do 8 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) 9 | end 10 | 11 | def init(:ok) do 12 | children = [ 13 | # FIXME: For some reason, under newer versions of the dependencies the project specifies, the Reagent socket acceptor is broken which prevents our other tests from running. So, for now, this is disabled. 14 | # supervisor(Reagent, [@peer_connection_pool_service, [name: @peer_connection_pool_service, port: 0]]), # the OS will pick a port on which we should listen 15 | supervisor(@discovery_service, [[name: @discovery_service, peer_connection_pool: @peer_connection_pool_service]]) 16 | ] 17 | 18 | supervise(children, strategy: :one_for_one) 19 | end 20 | end -------------------------------------------------------------------------------- /lib/bitcoin/node/peers/connection_pool.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Node.Peers.ConnectionPool do 2 | use Reagent 3 | 4 | def start(connection) do 5 | GenServer.start(__MODULE__, connection, name: __MODULE__) 6 | end 7 | 8 | use GenServer 9 | 10 | def init(connection) do 11 | {:ok, connection} 12 | end 13 | 14 | require Lager 15 | 16 | def add_peer(peer) do 17 | Lager.info "Peer connection pool received request to add peer: #{inspect(peer)}" 18 | end 19 | 20 | # Server 21 | 22 | # this message is sent when the socket has been completely accepted and the 23 | # process has been made owner of the socket, you don't need to wait for it 24 | # when implementing handle because it's internally handled 25 | def handle_info({ Reagent, :ack }, connection) do 26 | Lager.info "New Peer Connection" 27 | connection |> Socket.active!() 28 | { :noreply, connection } 29 | end 30 | 31 | def handle_info({ :tcp, _, data }, connection) do 32 | connection |> Socket.Stream.send!(data) 33 | { :noreply, connection } 34 | end 35 | 36 | def handle_info({ :tcp_closed, _ }, connection) do 37 | Lager.info "Closed Peer Connection" 38 | { :stop, :normal, connection } 39 | end 40 | 41 | end -------------------------------------------------------------------------------- /lib/bitcoin/node/peers/discovery.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Node.Peers.Discovery do 2 | require Lager 3 | use GenServer 4 | 5 | alias Bitcoin.Models.Peer 6 | 7 | def start_link(opts) do 8 | GenServer.start_link(__MODULE__, {:ok, opts}, opts) 9 | end 10 | 11 | def init({:ok, opts}) do 12 | # begin_discovery(opts[:peer_connection_pool]) 13 | { :ok, opts } 14 | end 15 | 16 | @moduledoc """ 17 | Implements DNS node discovery. 18 | 19 | from Satoshi C Client (chainparams.cpp): 20 | * alexykot.me 21 | * bitcoin.petertodd.org 22 | * bluematt.me 23 | * bitcoin.schildbach.de 24 | 25 | https://en.bitcoin.it/wiki/Satoshi_Client_Node_Discovery#DNS_Addresses 26 | """ 27 | defmodule Strategy.DNS do 28 | 29 | require Lager 30 | 31 | @domains [ 32 | [ "bitcoin.sipa.be", 'seed.bitcoin.sipa.be' ], # Pieter Wuille 33 | [ "bluematt.me", 'dnsseed.bluematt.me' ], # Matt Corallo 34 | [ "dashjr.org", 'dnsseed.bitcoin.dashjr.org' ], # Luke Dashjr 35 | [ "bitcoinstats.com", 'seed.bitcoinstats.com' ], # Christian Decker 36 | [ "xf2.org", 'bitseed.xf2.org' ], # Jeff Garzik 37 | [ "bitcoin.jonasschnelli.ch", 'seed.bitcoin.jonasschnelli.ch' ] # Jonas Schnelli 38 | ] 39 | 40 | def gather_peers(peer_pool) do 41 | 42 | Enum.map(@domains, fn([seed_name, domain]) -> 43 | Lager.info("Starting Peer Discovery via DNS for seed #{seed_name} at domain #{domain}") 44 | Enum.each(:inet_res.lookup(domain, :in, :a), fn(peer) -> 45 | peer_pool.add_peer(%Peer{ip_address: peer}) 46 | end) 47 | end) 48 | 49 | end 50 | 51 | end 52 | 53 | # Public Interface 54 | def begin_discovery(peer_connection_pool) do 55 | GenServer.cast(__MODULE__, {:begin_discovery, peer_connection_pool}) 56 | end 57 | 58 | def handle_cast({:begin_discovery, peer_connection_pool}, state) do 59 | Lager.info "Beginning Peer Discovery Process" 60 | Strategy.DNS.gather_peers(peer_connection_pool) 61 | {:noreply, state} 62 | end 63 | 64 | end -------------------------------------------------------------------------------- /lib/bitcoin/protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol do 2 | end -------------------------------------------------------------------------------- /lib/bitcoin/protocol/message.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Message do 2 | 3 | @moduledoc """ 4 | https://en.bitcoin.it/wiki/Protocol_documentation#Message_structure 5 | """ 6 | 7 | defimpl String.Chars, for: Bitcoin.Protocol.Message do 8 | 9 | @spec to_string(Message) :: String.t 10 | def to_string(item) do 11 | """ 12 | Bitcoin Protocol Message 13 | === 14 | 15 | Message Header 16 | --- 17 | #{item.header} 18 | 19 | Payload 20 | --- 21 | #{item.payload.to_string()} 22 | 23 | """ 24 | end 25 | 26 | end 27 | 28 | defstruct header: Bitcoin.Protocol.Message.Header, 29 | message: Bitcoin.Protocol.Message.Payload 30 | 31 | @type t :: %{ 32 | header: Bitcoin.Protocol.Message.Header.t, 33 | message: Bitcoin.Protocol.Message.Payload.t 34 | } 35 | 36 | alias Bitcoin.Protocol.Messages 37 | 38 | @command_handlers %{ 39 | "addr" => Messages.Addr, 40 | "alert" => Messages.Alert, 41 | "block" => Messages.Block, 42 | "getaddr" => Messages.GetAddr, 43 | "getblocks" => Messages.GetBlocks, 44 | "getdata" => Messages.GetData, 45 | "getheaders" => Messages.GetHeaders, 46 | "headers" => Messages.Headers, 47 | "inv" => Messages.Inv, 48 | "notfound" => Messages.NotFound, 49 | "ping" => Messages.Ping, 50 | "pong" => Messages.Pong, 51 | "reject" => Messages.Reject, 52 | "tx" => Messages.Tx, 53 | "verack" => Messages.Verack, 54 | "version" => Messages.Version 55 | } 56 | 57 | defmodule Payload do 58 | 59 | defimpl String.Chars, for: Payload do 60 | @spec to_string(Payload.t) :: String.t 61 | def to_string(data) do 62 | """ 63 | parsed data: 64 | #{data.payload |> String.Chars.to_string()} 65 | raw data: 66 | #{"0x" <> Base.encode16(data.raw_data)} 67 | """ |> String.strip() 68 | end 69 | end 70 | 71 | defstruct raw_data: <<>>, 72 | message: <<>> 73 | 74 | @type t :: %Payload{ 75 | raw_data: binary, 76 | message: binary 77 | } 78 | 79 | def parse(command, data) do 80 | message = case Bitcoin.Protocol.Message.handler(command) do 81 | # Unrecognized message 82 | nil -> <<>> 83 | # Parse using message specific module 84 | handler -> handler.parse(data) 85 | end 86 | 87 | %Payload{ 88 | raw_data: data, 89 | message: message 90 | } 91 | end 92 | 93 | end 94 | 95 | defmodule Header do 96 | 97 | @known_network_identifiers %{ 98 | main: <<0xF9, 0xBE, 0xB4, 0xD9>>, 99 | testnet: <<0xFA, 0xBF, 0xB5, 0xDA>>, 100 | testnet3: <<0x0B, 0x11, 0x09, 0x07>>, 101 | namecoin: <<0xF9, 0xBE, 0xB4, 0xFE>> 102 | } 103 | 104 | defstruct network_identifier: 0, 105 | command: <<>>, 106 | payload_size_bytes: 0, 107 | checksum: 0 108 | 109 | @type t :: %Header{ 110 | network_identifier: non_neg_integer, 111 | command: String.t, 112 | payload_size_bytes: non_neg_integer, 113 | checksum: non_neg_integer # sha256(sha256(payload)) first four bytes 114 | } 115 | 116 | def parse(<>) do 121 | 122 | %Header{ 123 | network_identifier: network_identifier, 124 | command: command |> String.trim_trailing(<<0>>), 125 | payload_size_bytes: payload_size_bytes, 126 | checksum: checksum 127 | } 128 | end 129 | 130 | end 131 | 132 | @doc """ 133 | Reads and deserialises bitcoin message in serialised format and returns the parsed result 134 | """ 135 | @spec parse(bitstring) :: Bitcoin.Protocol.Message.t 136 | def parse(message) do 137 | 138 | <> = message 141 | 142 | header = Header.parse(raw_header) 143 | 144 | %{ 145 | header: header, 146 | payload: Payload.parse(header.command, payload) 147 | } 148 | 149 | end 150 | 151 | @doc """ 152 | Returns module which can parse and build messages with specified command 153 | """ 154 | def handler(command), do: @command_handlers[command] 155 | 156 | end 157 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/addr.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Addr do 2 | 3 | @moduledoc """ 4 | Provide information on known nodes of the network. Non-advertised nodes should be forgotten after typically 3 hours. 5 | 6 | https://en.bitcoin.it/wiki/Protocol_documentation#addr 7 | """ 8 | 9 | alias Bitcoin.Protocol.Types.Integer 10 | alias Bitcoin.Protocol.Types.NetworkAddress 11 | 12 | defstruct address_list: [] 13 | 14 | @type t :: %__MODULE__{ 15 | address_list: [NetworkAddress] 16 | } 17 | 18 | def parse(data) do 19 | 20 | [count, payload] = Integer.parse_stream(data) 21 | 22 | address_list = if count > 0 do 23 | 24 | [addrs, _] = Enum.reduce(1..count, [[], payload], fn (_, [addr_collection, payload]) -> 25 | [element, payload] = NetworkAddress.parse_stream(payload) 26 | [addr_collection ++ [element], payload] 27 | end) 28 | 29 | addrs 30 | 31 | else 32 | 33 | [] 34 | 35 | end 36 | 37 | %__MODULE__{ 38 | address_list: address_list 39 | } 40 | 41 | end 42 | 43 | def serialize(%__MODULE__{} = s) do 44 | Integer.serialize(s.address_list |> Enum.count) 45 | <> ( 46 | s.address_list 47 | |> Enum.map(&NetworkAddress.serialize/1) 48 | |> Enum.reduce(<<>>, &(&2 <> &1)) 49 | ) 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/alert.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Alert do 2 | 3 | @moduledoc """ 4 | An alert message. 5 | https://en.bitcoin.it/wiki/Protocol_documentation#alert 6 | """ 7 | 8 | 9 | @known_valid_alert_signing_pubkeys %{ 10 | satoshi_client: Base.decode16!("04FC9702847840AAF195DE8442EBECEDF5B095CDBB9BC716BDA9110971B28A49E0EAD8564FF0DB22209E0374782C093BB899692D524E9D6A6956E7C5ECBCD68284") 11 | } 12 | 13 | defstruct signature: <<>>, # An ECDSA signature of the message 14 | payload: <<>>, # Serialized alert payload (raw) 15 | version: 0, # Alert format version 16 | relay_until: 0, # The timestamp beyond which nodes should stop relaying this alert 17 | expiration: 0, # The timestamp beyond which this alert is no longer in effect and should be ignored 18 | id: 0, # A unique ID number for this alert 19 | cancel: 0, # All alerts with an ID number less than or equal to this number should be cancelled: deleted and not accepted in the future 20 | set_cancel: 0, # All alert IDs contained in this set should be cancelled as above 21 | min_ver: 0, # This alert only applies to versions greater than or equal to this version. Other versions should still relay it. 22 | max_ver: 0, # This alert only applies to versions less than or equal to this version. Other versions should still relay it. 23 | set_sub_ver: 0, # If this set contains any elements, then only nodes that have their subVer contained in this set are affected by the alert. Other versions should still relay it. 24 | priority: 0, # Relative priority compared to other alerts 25 | comment: 0, # A comment on the alert that is not displayed 26 | status_bar: 0, # The alert message that is displayed to the user 27 | reserved: 0 # Reserved for future use 28 | 29 | @type t :: %Bitcoin.Protocol.Messages.Alert{ 30 | 31 | signature: bitstring, 32 | payload: bitstring, 33 | 34 | version: non_neg_integer, # 32-bit unsigned Integer, little endian 35 | relay_until: non_neg_integer, # 64-bit unsigned Integer, native 36 | expiration: non_neg_integer, # 64-bit unsigned Integer, native 37 | id: non_neg_integer, # 32-bit unsigned Integer, little endian 38 | cancel: non_neg_integer, # 32-bit unsigned Integer, little endian# 39 | set_cancel: non_neg_integer, 40 | min_ver: non_neg_integer, 41 | max_ver: non_neg_integer, 42 | set_sub_ver: [], 43 | 44 | priority: non_neg_integer, 45 | 46 | comment: String.t, 47 | status_bar: String.t, 48 | reserved: String.t 49 | 50 | } 51 | 52 | def parse(data) do 53 | 54 | [alert_payload_bytes, alert_data] = Bitcoin.Protocol.Types.Integer.parse_stream(data) 55 | 56 | << alert_payload :: bytes-size(alert_payload_bytes), alert_signature_payload :: binary >> = alert_data 57 | 58 | << 59 | version :: unsigned-little-integer-size(32), 60 | relay_until :: unsigned-native-integer-size(64), 61 | expiration :: unsigned-native-integer-size(64), 62 | id :: unsigned-little-integer-size(32), 63 | cancel :: unsigned-little-integer-size(32), 64 | remaining_payload :: binary 65 | >> = alert_payload 66 | 67 | [set_cancel, set_cancel_payload] = Bitcoin.Protocol.Types.IntegerArray.parse_stream(remaining_payload) 68 | 69 | << 70 | min_ver :: unsigned-little-integer-size(32), 71 | max_ver :: unsigned-little-integer-size(32), 72 | ver_payload :: binary 73 | >> = set_cancel_payload 74 | 75 | [set_sub_ver, sub_ver_payload] = Bitcoin.Protocol.Types.StringArray.parse_stream(ver_payload) 76 | 77 | << 78 | priority :: unsigned-little-integer-size(32), 79 | priority_payload :: binary 80 | >> = sub_ver_payload 81 | 82 | [comment, comment_payload] = Bitcoin.Protocol.Types.String.parse_stream(priority_payload) 83 | 84 | [status_bar, status_bar_payload] = Bitcoin.Protocol.Types.String.parse_stream(comment_payload) 85 | 86 | [reserved, _] = Bitcoin.Protocol.Types.String.parse_stream(status_bar_payload) 87 | 88 | [alert_signature_bytes, alert_signature_payload] = Bitcoin.Protocol.Types.Integer.parse_stream(alert_signature_payload) 89 | 90 | << alert_signature :: bytes-size(alert_signature_bytes) >> = alert_signature_payload 91 | 92 | %Bitcoin.Protocol.Messages.Alert{ 93 | signature: alert_signature, 94 | payload: alert_payload, 95 | 96 | version: version, 97 | relay_until: relay_until, 98 | expiration: expiration, 99 | id: id, 100 | cancel: cancel, 101 | set_cancel: set_cancel, 102 | 103 | min_ver: min_ver, 104 | max_ver: max_ver, 105 | set_sub_ver: set_sub_ver, 106 | 107 | priority: priority, 108 | 109 | comment: comment, 110 | status_bar: status_bar, 111 | reserved: reserved 112 | } 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/block.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Block do 2 | 3 | @moduledoc """ 4 | The block message is sent in response to a getdata message which requests transaction information from a block hash. 5 | 6 | The SHA256 hash that identifies each block (and which must have a run of 0 bits) is calculated from the first 6 7 | fields of this structure (version, prev_block, merkle_root, timestamp, bits, nonce, and standard SHA256 padding, 8 | making two 64-byte chunks in all) and not from the complete block. To calculate the hash, only two chunks need to 9 | be processed by the SHA256 algorithm. Since the nonce field is in the second chunk, the first chunk stays constant 10 | during mining and therefore only the second chunk needs to be processed. However, a Bitcoin hash is the hash of the 11 | hash, so two SHA256 rounds are needed for each mining iteration. See Block hashing algorithm 12 | for details and an example. 13 | 14 | https://en.bitcoin.it/wiki/Protocol_documentation#block 15 | """ 16 | 17 | alias Bitcoin.Protocol.Types.Integer 18 | alias Bitcoin.Protocol.Messages.Tx 19 | 20 | defstruct version: 0, # Block version information, based upon the software version creating this block 21 | previous_block: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>, # char[32], The hash value of the previous block this particular block references 22 | merkle_root: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>, # char[32], The reference to a Merkle tree collection which is a hash of all transactions related to this block 23 | timestamp: 0, # uint32_t, A Unix timestamp recording when this block was created (Currently limited to dates before the year 2106!) 24 | bits: 0, # uint32_t, The calculated difficulty target being used for this block 25 | nonce: 0, # uint32_t, The nonce used to generate this block… to allow variations of the header and compute different hashes 26 | transactions: [] # count - Bitcoin.Protocol.Types.Integer, number of transaction entries in this block, [Transaction] 27 | 28 | @type t :: %__MODULE__{ 29 | version: integer, 30 | previous_block: bitstring, 31 | merkle_root: bitstring, 32 | timestamp: non_neg_integer, 33 | bits: non_neg_integer, 34 | nonce: non_neg_integer, 35 | transactions: [Tx] 36 | } 37 | 38 | def parse(data) do 39 | 40 | <> = data 47 | 48 | [transaction_count, payload] = Integer.parse_stream(payload) 49 | 50 | [transactions, _] = Enum.reduce(1..transaction_count, [[], payload], fn (_, [collection, payload]) -> 51 | [element, payload] = Tx.parse_stream(payload) 52 | [collection ++ [element], payload] 53 | end) 54 | 55 | %__MODULE__{ 56 | version: version, 57 | previous_block: previous_block, 58 | merkle_root: merkle_root, 59 | timestamp: timestamp, 60 | bits: bits, 61 | nonce: nonce, 62 | transactions: transactions 63 | } 64 | 65 | end 66 | 67 | def serialize(%__MODULE__{} = s) do 68 | << 69 | s.version :: little-integer-size(32), 70 | s.previous_block :: bytes-size(32), 71 | s.merkle_root :: bytes-size(32), 72 | s.timestamp :: unsigned-little-integer-size(32), 73 | s.bits :: unsigned-little-integer-size(32), 74 | s.nonce :: unsigned-little-integer-size(32), 75 | >> <> 76 | Bitcoin.Protocol.Types.Integer.serialize(s.transactions |> Enum.count) 77 | <> ( 78 | s.transactions 79 | |> Enum.map(&Tx.serialize/1) 80 | |> Enum.reduce(<<>>, &(&2 <> &1)) 81 | ) 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/get_addr.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.GetAddr do 2 | 3 | @moduledoc """ 4 | The getaddr message sends a request to a node asking for information about known active peers to help with finding 5 | potential nodes in the network. The response to receiving this message is to transmit one or more addr messages with 6 | one or more peers from a database of known active peers. The typical presumption is that a node is likely to be 7 | active if it has been sending a message within the last three hours. 8 | 9 | No additional data is transmitted with this message. 10 | 11 | https://en.bitcoin.it/wiki/Protocol_specification#getaddr 12 | """ 13 | 14 | def parse(_data) do 15 | %{} 16 | end 17 | 18 | def serialize(_), do: <<>> 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/get_blocks.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.GetBlocks do 2 | 3 | @moduledoc """ 4 | Return an inv packet containing the list of blocks starting right after the last known hash in the block locator 5 | object, up to hash_stop or 500 blocks, whichever comes first. 6 | 7 | The locator hashes are processed by a node in the order as they appear in the message. If a block hash is found in 8 | the node's main chain, the list of its children is returned back via the inv message and the remaining locators are 9 | ignored, no matter if the requested limit was reached, or not. 10 | 11 | To receive the next blocks hashes, one needs to issue getblocks again with a new block locator object. Keep in mind 12 | that some clients may provide blocks which are invalid if the block locator object contains a hash on the invalid 13 | branch. 14 | 15 | To create the block locator hashes, keep pushing hashes until you go back to the genesis block. 16 | After pushing 10 hashes back, the step backwards doubles every loop. 17 | 18 | Note that it is allowed to send in fewer known hashes down to a minimum of just one hash. However, the purpose of 19 | the block locator object is to detect a wrong branch in the caller's main chain. If the peer detects that you are 20 | off the main chain, it will send in block hashes which are earlier than your last known block. So if you just send 21 | in your last known hash and it is off the main chain, the peer starts over at block #1. 22 | 23 | https://en.bitcoin.it/wiki/Protocol_specification#getblocks 24 | """ 25 | 26 | alias Bitcoin.Protocol.Types.Integer 27 | 28 | defstruct version: 0, # the protocol version 29 | block_locator_hashes: [], # block locator object; newest back to genesis block (dense to start, but then sparse) 30 | hash_stop: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>> # hash of the last desired block; set to zero to get as many blocks as possible (up to 500) 31 | 32 | @type t :: %__MODULE__{ 33 | version: non_neg_integer, 34 | block_locator_hashes: list, 35 | hash_stop: bitstring 36 | } 37 | 38 | def parse(data) do 39 | 40 | << version :: unsigned-little-integer-size(32), payload :: binary>> = data 41 | 42 | [count, payload] = Integer.parse_stream(payload) 43 | 44 | [block_locator_hashes, payload] = Enum.reduce(1..count, [[], payload], fn (_, [collection, payload]) -> 45 | <> = payload 46 | [collection ++ [element], payload] 47 | end) 48 | 49 | << hash_stop :: bytes-size(32) >> = payload 50 | 51 | %__MODULE__{ 52 | version: version, 53 | block_locator_hashes: block_locator_hashes, 54 | hash_stop: hash_stop 55 | } 56 | 57 | end 58 | 59 | def serialize(%__MODULE__{} = s) do 60 | << 61 | s.version :: unsigned-little-integer-size(32), 62 | >> <> 63 | Integer.serialize(s.block_locator_hashes |> Enum.count) 64 | <> ( 65 | s.block_locator_hashes |> Enum.reduce(<<>>, &(&2 <> &1)) 66 | ) <> 67 | << 68 | s.hash_stop :: bytes-size(32) 69 | >> 70 | end 71 | 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/get_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.GetData do 2 | 3 | @moduledoc """ 4 | getdata is used in response to inv, to retrieve the content of a specific object, and is usually sent after 5 | receiving an inv packet, after filtering known elements. It can be used to retrieve transactions, but only 6 | if they are in the memory pool or relay set - arbitrary access to transactions in the chain is not allowed 7 | to avoid having clients start to depend on nodes having full transaction indexes (which modern nodes do not). 8 | 9 | https://en.bitcoin.it/wiki/Protocol_specification#getdata 10 | """ 11 | 12 | alias Bitcoin.Protocol.Types.Integer 13 | alias Bitcoin.Protocol.Types.InventoryVector 14 | 15 | defstruct inventory_vectors: [] 16 | 17 | @type t :: %__MODULE__{ 18 | inventory_vectors: [InventoryVector] 19 | } 20 | 21 | def parse(data) do 22 | 23 | [count, payload] = Integer.parse_stream(data) 24 | 25 | inventory_vectors = if count > 0 do 26 | 27 | [vects, _] = Enum.reduce(1..count, [[], payload], fn (_, [collection, payload]) -> 28 | [element, payload] = InventoryVector.parse_stream(payload) 29 | [collection ++ [element], payload] 30 | end) 31 | 32 | vects 33 | 34 | else 35 | 36 | [] 37 | 38 | end 39 | 40 | %__MODULE__{ 41 | inventory_vectors: inventory_vectors 42 | } 43 | 44 | end 45 | 46 | def serialize(%__MODULE__{} = s) do 47 | (s.inventory_vectors |> Enum.count |> Integer.serialize) 48 | <> ( 49 | s.inventory_vectors 50 | |> Enum.map(&InventoryVector.serialize/1) 51 | |> Enum.reduce(<<>>, &(&2 <> &1)) 52 | ) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/get_headers.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.GetHeaders do 2 | 3 | @moduledoc """ 4 | Return a headers packet containing the headers of blocks starting right after the last known hash in the block 5 | locator object, up to hash_stop or 2000 blocks, whichever comes first. To receive the next block headers, one needs 6 | to issue getheaders again with a new block locator object. The getheaders command is used by thin clients to 7 | quickly download the block chain where the contents of the transactions would be irrelevant (because they are not 8 | ours). Keep in mind that some clients may provide headers of blocks which are invalid if the block locator object 9 | contains a hash on the invalid branch. 10 | 11 | For the block locator object in this packet, the same rules apply as for the getblocks packet. 12 | 13 | https://en.bitcoin.it/wiki/Protocol_specification#getheaders 14 | """ 15 | 16 | alias Bitcoin.Protocol.Types.Integer 17 | 18 | defstruct version: 0, # the protocol version 19 | block_locator_hashes: [], # block locator object; newest back to genesis block (dense to start, but then sparse) 20 | hash_stop: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>> # hash of the last desired block; set to zero to get as many headers as possible (up to 2000) 21 | 22 | @type t :: %__MODULE__{ 23 | version: non_neg_integer, 24 | block_locator_hashes: list, 25 | hash_stop: bitstring 26 | } 27 | 28 | def parse(data) do 29 | 30 | <> = data 31 | 32 | [count, payload] = Integer.parse_stream(payload) 33 | 34 | [block_locator_hashes, payload] = Enum.reduce(1..count, [[], payload], fn (_, [collection, payload]) -> 35 | <> = payload 36 | [collection ++ [element], payload] 37 | end) 38 | 39 | << hash_stop :: bytes-size(32) >> = payload 40 | 41 | %__MODULE__{ 42 | version: version, 43 | block_locator_hashes: block_locator_hashes, 44 | hash_stop: hash_stop 45 | } 46 | 47 | end 48 | 49 | def serialize(%__MODULE__{} = s) do 50 | << 51 | s.version :: unsigned-little-integer-size(32), 52 | >> <> 53 | Integer.serialize(s.block_locator_hashes |> Enum.count) 54 | <> ( 55 | s.block_locator_hashes |> Enum.reduce(<<>>, &(&2 <> &1)) 56 | ) <> 57 | << 58 | s.hash_stop :: bytes-size(32) 59 | >> 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/headers.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Headers do 2 | 3 | @moduledoc """ 4 | The headers packet returns block headers in response to a getheaders packet. 5 | 6 | Note that the block headers in this packet include a transaction count (a var_int, so there can be more than 81 7 | bytes per header) as opposed to the block headers which are sent to miners. 8 | 9 | https://en.bitcoin.it/wiki/Protocol_documentation#headers 10 | 11 | 12 | Block headers: each 80-byte block header is in the format described in the block headers section with an additional 0x00 suffixed. This 0x00 is called the transaction count, but because the headers message doesn’t include any transactions, the transaction count is always zero. 13 | 14 | https://bitcoin.org/en/developer-reference#headers 15 | """ 16 | 17 | alias Bitcoin.Protocol.Types.Integer 18 | alias Bitcoin.Protocol.Types.BlockHeader 19 | 20 | defstruct headers: [] # Bitcoin.Protocol.Types.BlockHeader[], https://en.bitcoin.it/wiki/Protocol_specification#Block_Headers 21 | 22 | @type t :: %Bitcoin.Protocol.Messages.Headers{ 23 | headers: [BlockHeader] 24 | } 25 | 26 | def parse(data) do 27 | 28 | [header_count, payload] = Integer.parse_stream(data) 29 | 30 | [headers, _] = Enum.reduce(1..header_count, [[], payload], fn (_, [collection, payload]) -> 31 | [element, payload] = BlockHeader.parse_stream(payload) 32 | [collection ++ [element], payload] 33 | end) 34 | 35 | %Bitcoin.Protocol.Messages.Headers{ 36 | headers: headers 37 | } 38 | 39 | end 40 | 41 | def serialize(%__MODULE__{} = s) do 42 | Integer.serialize(s.headers |> Enum.count) 43 | <> ( 44 | s.headers 45 | |> Enum.map(&BlockHeader.serialize/1) 46 | |> Enum.reduce(<<>>, &(&2 <> &1)) 47 | ) 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/inv.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Inv do 2 | 3 | @moduledoc """ 4 | Allows a node to advertise its knowledge of one or more objects. It can be received unsolicited, or in reply to getblocks. 5 | 6 | https://en.bitcoin.it/wiki/Protocol_documentation#inv 7 | """ 8 | 9 | alias Bitcoin.Protocol.Types.Integer 10 | alias Bitcoin.Protocol.Types.InventoryVector 11 | 12 | defstruct inventory_vectors: [] 13 | 14 | @type t :: %__MODULE__{ 15 | inventory_vectors: [InventoryVector] 16 | } 17 | 18 | def parse(data) do 19 | 20 | [count, payload] = Integer.parse_stream(data) 21 | 22 | inventory_vectors = if count > 0 do 23 | 24 | [vects, _] = Enum.reduce(1..count, [[], payload], fn (_, [collection, payload]) -> 25 | [element, payload] = InventoryVector.parse_stream(payload) 26 | [collection ++ [element], payload] 27 | end) 28 | 29 | vects 30 | 31 | else 32 | 33 | [] 34 | 35 | end 36 | 37 | %__MODULE__{ 38 | inventory_vectors: inventory_vectors 39 | } 40 | 41 | end 42 | 43 | def serialize(%__MODULE__{} = s) do 44 | Integer.serialize(s.inventory_vectors |> Enum.count) 45 | <> ( 46 | s.inventory_vectors 47 | |> Enum.map(&InventoryVector.serialize/1) 48 | |> Enum.reduce(<<>>, &(&2 <> &1)) 49 | ) 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/not_found.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.NotFound do 2 | 3 | @moduledoc """ 4 | notfound is a response to a getdata, sent if any requested data items could not be relayed, for example, because 5 | the requested transaction was not in the memory pool or relay set. 6 | 7 | https://en.bitcoin.it/wiki/Protocol_specification#notfound 8 | """ 9 | 10 | alias Bitcoin.Protocol.Types.Integer 11 | alias Bitcoin.Protocol.Types.InventoryVector 12 | 13 | defstruct inventory_vectors: [] 14 | 15 | @type t :: %Bitcoin.Protocol.Messages.NotFound{ 16 | inventory_vectors: [InventoryVector] 17 | } 18 | 19 | def parse(data) do 20 | 21 | [count, payload] = Integer.parse_stream(data) 22 | 23 | inventory_vectors = if count > 0 do 24 | 25 | [vects, _] = Enum.reduce(1..count, [[], payload], fn (_, [collection, payload]) -> 26 | [element, payload] = InventoryVector.parse_stream(payload) 27 | [collection ++ [element], payload] 28 | end) 29 | 30 | vects 31 | 32 | else 33 | 34 | [] 35 | 36 | end 37 | 38 | %Bitcoin.Protocol.Messages.NotFound{ 39 | inventory_vectors: inventory_vectors 40 | } 41 | 42 | end 43 | 44 | def serialize(%__MODULE__{} = s) do 45 | Integer.serialize(s.inventory_vectors |> Enum.count) 46 | <> ( 47 | s.inventory_vectors 48 | |> Enum.map(&InventoryVector.serialize/1) 49 | |> Enum.reduce(<<>>, &(&2 <> &1)) 50 | ) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/ping.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Ping do 2 | 3 | @moduledoc """ 4 | The ping message is sent primarily to confirm that the TCP/IP connection is still valid. An error in transmission 5 | is presumed to be a closed connection and the address is removed as a current peer. 6 | 7 | https://en.bitcoin.it/wiki/Protocol_specification#ping 8 | """ 9 | 10 | defstruct nonce: 0 # random nonce 11 | 12 | @type t :: %__MODULE__{ 13 | nonce: non_neg_integer 14 | } 15 | 16 | def parse(<>) do 17 | 18 | %__MODULE__{ 19 | nonce: nonce 20 | } 21 | 22 | end 23 | 24 | def serialize(%__MODULE__{} = s) do 25 | << 26 | s.nonce :: unsigned-little-integer-size(64) 27 | >> 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/pong.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Pong do 2 | 3 | @moduledoc """ 4 | The pong message is sent in response to a ping message. In modern protocol versions, a pong response is generated 5 | using a nonce included in the ping. 6 | 7 | https://en.bitcoin.it/wiki/Protocol_specification#pong 8 | """ 9 | 10 | defstruct nonce: 0 # nonce from received ping 11 | 12 | @type t :: %__MODULE__{ 13 | nonce: non_neg_integer 14 | } 15 | 16 | def parse(<>) do 17 | 18 | %__MODULE__{ 19 | nonce: nonce 20 | } 21 | 22 | end 23 | 24 | def serialize(%__MODULE__{} = s) do 25 | << 26 | s.nonce :: unsigned-little-integer-size(64) 27 | >> 28 | end 29 | 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/reject.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Reject do 2 | 3 | @moduledoc """ 4 | The reject message is sent when messages are rejected. 5 | 6 | https://en.bitcoin.it/wiki/Protocol_specification#reject 7 | """ 8 | 9 | alias Bitcoin.Protocol.Types.String 10 | 11 | @reject_reasons %{ 12 | 0x01 => :MALFORMED, 13 | 0x10 => :INVALID, 14 | 0x11 => :OBSOLETE, 15 | 0x12 => :DUPLICATE, 16 | 0x40 => :NONSTANDARD, 17 | 0x41 => :DUST, 18 | 0x42 => :INSUFFICIENTFEE, 19 | 0x43 => :CHECKPOINT 20 | } 21 | 22 | defstruct message: "", # type of message rejected 23 | code: 0, # code relating to the rejected message 24 | reason: "", # text version of the reason for rejection 25 | data: <<>> # Optional extra data provided by some errors. Currently, all errors which provide this field 26 | # fill it with the TXID or block header hash of the object being rejected, so the field is 32 bytes. 27 | 28 | @type t :: %Bitcoin.Protocol.Messages.Reject{ 29 | message: bitstring, 30 | code: non_neg_integer, 31 | reason: bitstring, 32 | data: { nil, bitstring } 33 | } 34 | 35 | def parse(data) do 36 | 37 | [message, payload] = String.parse_stream(data) 38 | <> = payload 39 | [reason, data] = String.parse_stream(payload) 40 | 41 | %Bitcoin.Protocol.Messages.Reject{ 42 | message: message, 43 | code: Map.get(@reject_reasons, code), 44 | reason: reason, 45 | data: data 46 | } 47 | 48 | end 49 | 50 | end -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/tx.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Tx do 2 | 3 | @moduledoc """ 4 | tx describes a bitcoin transaction, in reply to getdata. 5 | 6 | https://en.bitcoin.it/wiki/Protocol_documentation#tx 7 | """ 8 | 9 | alias Bitcoin.Protocol.Types.Integer 10 | alias Bitcoin.Protocol.Types.TransactionInput 11 | alias Bitcoin.Protocol.Types.TransactionOutput 12 | 13 | defstruct version: 0, # Transaction data format version 14 | inputs: [], # A list of 1 or more transaction inputs or sources for coins 15 | outputs: [], # A list of 1 or more transaction outputs or destinations for coins 16 | lock_time: 0 # The block number or timestamp at which this transaction is locked: 17 | # 0 - Not Locked 18 | # < 500000000 - Block number at which this transaction is locked 19 | # >= 500000000 - UNIX timestamp at which this transaction is locked 20 | # If all TxIn inputs have final (0xffffffff) sequence numbers then lock_time is irrelevant. 21 | # Otherwise, the transaction may not be added to a block until after lock_time (see NLockTime). 22 | 23 | @type t :: %__MODULE__{ 24 | version: integer, # note, this is signed 25 | inputs: [], 26 | outputs: [], 27 | lock_time: non_neg_integer 28 | } 29 | 30 | def parse_stream(data) do 31 | 32 | <> = data 33 | 34 | [tx_in_count, payload] = Integer.parse_stream(payload) 35 | 36 | [transaction_inputs, payload] = Enum.reduce(1..tx_in_count, [[], payload], fn (_, [collection, payload]) -> 37 | [element, payload] = TransactionInput.parse_stream(payload) 38 | [collection ++ [element], payload] 39 | end) 40 | 41 | [tx_out_count, payload] = Integer.parse_stream(payload) 42 | 43 | [transaction_outputs, payload] = Enum.reduce(1..tx_out_count, [[], payload], fn (_, [collection, payload]) -> 44 | [element, payload] = TransactionOutput.parse_stream(payload) 45 | [collection ++ [element], payload] 46 | end) 47 | 48 | <> = payload 49 | 50 | struct = %__MODULE__{ 51 | version: version, 52 | inputs: transaction_inputs, 53 | outputs: transaction_outputs, 54 | lock_time: lock_time 55 | } 56 | 57 | [struct, remaining] 58 | 59 | end 60 | 61 | def parse(data) do 62 | [struct, ""] = parse_stream(data) 63 | struct 64 | end 65 | 66 | def serialize(%__MODULE__{} = s) do 67 | << 68 | s.version :: little-integer-size(32), 69 | >> <> 70 | Integer.serialize(s.inputs |> Enum.count) 71 | <> ( 72 | s.inputs 73 | |> Enum.map(&TransactionInput.serialize/1) 74 | |> Enum.reduce(<<>>, &(&2 <> &1)) 75 | ) <> 76 | Integer.serialize(s.outputs |> Enum.count) 77 | <> ( 78 | s.outputs 79 | |> Enum.map(&TransactionOutput.serialize/1) 80 | |> Enum.reduce(<<>>, &(&2 <> &1)) 81 | ) <> 82 | << s.lock_time :: unsigned-little-integer-size(32) >> 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/verack.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Verack do 2 | 3 | @moduledoc """ 4 | The verack message is sent in reply to version. 5 | This message consists of only a message header with the command string "verack". 6 | 7 | https://en.bitcoin.it/wiki/Protocol_specification#verack 8 | """ 9 | 10 | def parse(_data) do 11 | %{} 12 | end 13 | 14 | def serialize(_), do: <<>> 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/messages/version.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Messages.Version do 2 | 3 | @moduledoc """ 4 | When a node creates an outgoing connection, it will immediately advertise its version. 5 | The remote node will respond with its version. 6 | No further communication is possible until both peers have exchanged their version. 7 | 8 | https://en.bitcoin.it/wiki/Protocol_documentation#version 9 | """ 10 | 11 | alias Bitcoin.Protocol.Types.String 12 | alias Bitcoin.Protocol.Types.NetworkAddress 13 | 14 | defstruct version: 0, # (int32_t) Identifies protocol version being used by the node 15 | services: <<1, 0, 0, 0, 0, 0, 0, 0>>, # (uint64_t) bitfield of features to be enabled for this connection 16 | timestamp: 0, # (int64_t) standard UNIX timestamp in seconds 17 | address_of_receiving_node: NetworkAddress, # The network address of the node receiving this message. - Bitcoin.Protocol.Types.NetworkAddress 18 | # versions 106 and greater, otherwise these fields do not exist 19 | address_of_sending_node: NetworkAddress, # The network address of the node emitting this message. - Bitcoin.Protocol.Types.NetworkAddress 20 | nonce: 0, # (uint64_t) Node random nonce, randomly generated every time a version packet is sent. This nonce is used to detect connections to self. 21 | user_agent: "", # User Agent (0x00 if string is 0 bytes long) 22 | start_height: 0, # (int32_t) The last block received by the emitting node 23 | relay: false # (bool) Whether the remote peer should announce relayed transactions or not, may be absent, see BIP 0037 , since version >= 70001 24 | 25 | @type t :: %Bitcoin.Protocol.Messages.Version{ 26 | version: non_neg_integer, 27 | services: bitstring, 28 | timestamp: non_neg_integer, 29 | address_of_receiving_node: binary, 30 | address_of_sending_node: binary, 31 | nonce: non_neg_integer, 32 | user_agent: String.t, 33 | start_height: non_neg_integer, 34 | relay: boolean 35 | } 36 | 37 | def parse(data) do 38 | 39 | <> = data 44 | 45 | [address_of_receiving_node, remaining] = NetworkAddress.parse_version_stream(remaining) 46 | [address_of_sending_node, remaining] = NetworkAddress.parse_version_stream(remaining) 47 | 48 | <> = remaining 51 | 52 | [user_agent, remaining] = String.parse_stream(remaining) 53 | 54 | <> = remaining 57 | 58 | relay = relay == <<1>> # Relay may be absent, see https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki 59 | 60 | %Bitcoin.Protocol.Messages.Version{ 61 | version: version, 62 | services: services, 63 | timestamp: timestamp, 64 | address_of_receiving_node: address_of_receiving_node, 65 | address_of_sending_node: address_of_sending_node, 66 | nonce: nonce, 67 | user_agent: user_agent, 68 | start_height: start_height, 69 | relay: relay 70 | } 71 | 72 | end 73 | 74 | def serialize(%Bitcoin.Protocol.Messages.Version{} = s) do 75 | << 76 | s.version :: unsigned-little-integer-size(32), 77 | s.services :: bitstring-size(64), 78 | s.timestamp :: unsigned-little-integer-size(64) 79 | >> 80 | <> NetworkAddress.serialize_version(s.address_of_receiving_node) 81 | <> NetworkAddress.serialize_version(s.address_of_sending_node) 82 | <> << 83 | s.nonce :: unsigned-little-integer-size(64), 84 | >> 85 | <> String.serialize(s.user_agent) 86 | <> << 87 | s.start_height :: unsigned-little-integer-size(32), 88 | (if s.relay, do: 1, else: 0) 89 | >> 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/types/block_header.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Types.BlockHeader do 2 | 3 | defstruct version: 0, # Block version information, based upon the software version creating this block 4 | previous_block: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>, # char[32], The hash value of the previous block this particular block references 5 | merkle_root: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>, # char[32], The reference to a Merkle tree collection which is a hash of all transactions related to this block 6 | timestamp: 0, # uint32_t, A Unix timestamp recording when this block was created (Currently limited to dates before the year 2106!) 7 | bits: 0, # uint32_t, The calculated difficulty target being used for this block 8 | nonce: 0, # uint32_t, The nonce used to generate this block… to allow variations of the header and compute different hashes 9 | transaction_count: 0 # count - Bitcoin.Protocol.Types.Integer, number of transaction entries in this block 10 | 11 | @type t :: %__MODULE__{ 12 | version: integer, 13 | previous_block: bitstring, 14 | merkle_root: bitstring, 15 | timestamp: non_neg_integer, 16 | bits: non_neg_integer, 17 | nonce: non_neg_integer, 18 | transaction_count: non_neg_integer 19 | } 20 | 21 | def parse(data) do 22 | <> = data 29 | 30 | [transaction_count, _] = Bitcoin.Protocol.Types.Integer.parse_stream(payload) 31 | 32 | %__MODULE__{ 33 | version: version, 34 | previous_block: previous_block, 35 | merkle_root: merkle_root, 36 | timestamp: timestamp, 37 | bits: bits, 38 | nonce: nonce, 39 | transaction_count: transaction_count 40 | } 41 | end 42 | 43 | def parse_stream(data) do 44 | <> = data 51 | 52 | [transaction_count, payload] = Bitcoin.Protocol.Types.Integer.parse_stream(payload) 53 | 54 | [%__MODULE__{ 55 | version: version, 56 | previous_block: previous_block, 57 | merkle_root: merkle_root, 58 | timestamp: timestamp, 59 | bits: bits, 60 | nonce: nonce, 61 | transaction_count: transaction_count 62 | }, payload] 63 | end 64 | 65 | def serialize(%__MODULE__{} = s) do 66 | << 67 | s.version :: little-integer-size(32), 68 | s.previous_block :: bytes-size(32), 69 | s.merkle_root :: bytes-size(32), 70 | s.timestamp :: unsigned-little-integer-size(32), 71 | s.bits :: unsigned-little-integer-size(32), 72 | s.nonce :: unsigned-little-integer-size(32), 73 | >> <> 74 | # https://en.bitcoin.it/wiki/Protocol_documentation#headers says tx_count can be > 0 75 | # https://bitcoin.org/en/developer-reference#headers says it's always 0x00 76 | # ¯\_(ツ)_/¯ 77 | Bitcoin.Protocol.Types.Integer.serialize(s.transaction_count) 78 | end 79 | 80 | 81 | 82 | end 83 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/types/integer.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Types.Integer do 2 | 3 | def parse(<<0xFD, data :: unsigned-little-integer-size(16)>>) do 4 | data 5 | end 6 | 7 | def parse(<<0xFE, data :: unsigned-little-integer-size(32)>>) do 8 | data 9 | end 10 | 11 | def parse(<<0xFF, data :: unsigned-native-integer-size(64)>>) do 12 | data 13 | end 14 | 15 | def parse(<>) do 16 | data 17 | end 18 | 19 | def parse_stream(<<0xFD, data :: unsigned-little-integer-size(16), remaining :: binary>>) do 20 | [data, remaining] 21 | end 22 | 23 | def parse_stream(<<0xFE, data :: unsigned-little-integer-size(32), remaining :: binary>>) do 24 | [data, remaining] 25 | end 26 | 27 | def parse_stream(<<0xFF, data :: unsigned-native-integer-size(64), remaining :: binary>>) do 28 | [data, remaining] 29 | end 30 | 31 | def parse_stream(<>) do 32 | [data, remaining] 33 | end 34 | 35 | def serialize(int) when is_integer(int) and int < 0xFD, do: << int :: unsigned-little-integer-size(8) >> 36 | def serialize(int) when is_integer(int) and int <= 0xFFFF, do: << 0xFD, int :: unsigned-little-integer-size(16) >> 37 | def serialize(int) when is_integer(int) and int <= 0xFFFF_FFFF, do: << 0xFE, int :: unsigned-little-integer-size(32) >> 38 | def serialize(int) when is_integer(int) and int > 0xFFFF_FFFF, do: << 0xFF, int :: unsigned-little-integer-size(64) >> 39 | 40 | end 41 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/types/integer_array.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Types.IntegerArray do 2 | 3 | alias Bitcoin.Protocol.Types.Integer 4 | 5 | def parse_stream(data) do 6 | 7 | [array_size, payload] = Integer.parse_stream(data) 8 | 9 | if array_size > 0 do 10 | 11 | Enum.reduce(1..array_size, [[], payload], fn (_, [element_collection, payload]) -> 12 | << element :: unsigned-little-integer-size(32), payload::binary >> = payload 13 | [element_collection ++ [element], payload] 14 | end) 15 | 16 | else 17 | 18 | [[], payload] 19 | 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/types/inventory_vector.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Types.InventoryVector do 2 | 3 | @inventory_vector_reference_types %{ 4 | 0 => :error, # 0 - Any data of with this number may be ignored 5 | 1 => :msg_tx, # 1 - Hash is related to a transaction 6 | 2 => :msg_block, # 2 - Hash is related to a data block 7 | 3 => :msg_filtered_block # 3 - Hash of a block header; identical to MSG_BLOCK. When used in a getdata message, this indicates the reply should be a merkleblock message rather than a block message; this only works if a bloom filter has been set. 8 | } 9 | 10 | defstruct reference_type: :error, 11 | hash: <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>> 12 | 13 | @type t :: %__MODULE__{ 14 | reference_type: atom, 15 | hash: String.t 16 | } 17 | 18 | def parse(<>) do 19 | %Bitcoin.Protocol.Types.InventoryVector{ 20 | reference_type: type_id |> get_type_name, 21 | hash: hash 22 | } 23 | end 24 | 25 | def parse_stream(<>) do 26 | [%Bitcoin.Protocol.Types.InventoryVector{ 27 | reference_type: type_id |> get_type_name, 28 | hash: hash 29 | }, remaining_stream] 30 | end 31 | 32 | def serialize(%__MODULE__{} = s) do 33 | type_id = s.reference_type |> get_type_id 34 | << 35 | type_id :: unsigned-little-integer-size(32), 36 | s.hash :: bytes-size(32) 37 | >> 38 | end 39 | 40 | defp get_type_id(type_name) do 41 | @inventory_vector_reference_types |> Enum.map(fn {k,v} -> {v,k} end) |> Enum.into(%{}) |> Map.get(type_name) 42 | end 43 | 44 | defp get_type_name(type_id) do 45 | @inventory_vector_reference_types |> Map.get(type_id) 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/bitcoin/protocol/types/network_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Bitcoin.Protocol.Types.NetworkAddress do 2 | 3 | defstruct time: 0, # (uint32) the Time (version >= 31402). Not present in version message. 4 | services: <<0, 0, 0, 0, 0, 0, 0, 0>>, # (uint64_t) bitfield of features to be enabled for this connection. See Version Message. 5 | address: {0, 0, 0, 0}, # decoded address tuple, 4 elemnt for IPv4, 8 element for IPv6 (see :inet) 6 | port: 8333 # (uint16_t) port number, network byte order 7 | 8 | @type t :: %Bitcoin.Protocol.Types.NetworkAddress{ 9 | time: non_neg_integer, 10 | services: binary, 11 | address: tuple, 12 | port: non_neg_integer 13 | } 14 | 15 | def parse(<