bitcoin_node 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 48d06efa48cc2005ed61b0f93c74b60c908a0e16
4
- data.tar.gz: c0a85ea6484d812575e186f853989e39161890a0
3
+ metadata.gz: 50c1b46b013075e9d27e561239c9ca3f03d50237
4
+ data.tar.gz: a165a27047b94ea084f55e761dfcfb939b3dca4e
5
5
  SHA512:
6
- metadata.gz: 71e234828fdcb3f467e4124aade08d5d4ba9c4b15bd45c63a3147b107cdc764413a2a2a774ccce8223d79d58b5e19d1d1a49755bb8fb9b4ff8de5b3e66071b4e
7
- data.tar.gz: 9d2503e3af79a1a705fc2283a4a45040c1a7a1f8f5da5718db422705440c942965e080fc3f388c9dead2e2eeea8ea92b27a3e98a3349586eab54c39d90f95890
6
+ metadata.gz: afea8a1965550c6c9d4b24eb6eead1895e8814d8818012759906430fc625e712b0d3d2139ce5a0f1d108b1b2a314d7d05649c4f9aff3eeccb87f2e10ea27da96
7
+ data.tar.gz: fd14859b97c3410cd825b6dce8a270eb8e00107d9946caf7093f58e12c6e4ad6a8435fc3502c458759c0836d347e8b26ddcd27a71d500df258affa5162bb4354
data/README.md CHANGED
@@ -1,32 +1,45 @@
1
1
  # BitcoinNode
2
2
 
3
- Study of Bitcoin protocol by implementing a simple node
4
- in the p2p bitcoin network
3
+ Study of Bitcoin protocol by implementing a simple node in the p2p bitcoin network.
5
4
 
6
- Using as much as Ruby stdlib as possible. Main foreign dependency is Celluloid::IO
5
+ Using as much as Ruby stdlib as possible. Main foreign dependency is `Celluloid::IO`
7
6
 
8
7
  ## Installation
9
8
 
10
- Add this line to your application's Gemfile:
9
+ Add to your Gemfile `gem 'bitcoin_node'` or locally run
11
10
 
12
- gem 'bitcoin_node'
11
+ $ gem install bitcoin_node
13
12
 
14
- And then execute:
13
+ Test
15
14
 
16
- $ bundle
15
+ $ bundle exec rspec
17
16
 
18
- Or install it yourself as:
17
+ ## Usage
19
18
 
20
- $ gem install bitcoin_node
19
+ ### Create messages
21
20
 
22
- ## Usage
21
+ ```ruby
22
+ require 'bitcoin_node'
23
+
24
+ ping = BitcoinNode::Protocol::Messages.ping
25
+ # => #<BitcoinNode::Protocol::Message:0x007feb24e1fa20 @payload=#<Ping {:nonce=>#<struct Integer64Field 12031756400052209357>}>, @command="ping">
26
+
27
+ ping.raw
28
+ # => "\xF9\xBE\xB4\xD9ping\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00\xAB\x0F\x0FZ\x95\xDC{\xA1\xB1i\x11]"
29
+
30
+ version = BN::Protocol::Messages.version
31
+ ```
32
+
33
+ ### Single client
34
+
35
+ ```ruby
36
+ require 'bitcoin_node'
23
37
 
24
- TODO: Write usage instructions here
38
+ host = '144.76.217.165'
25
39
 
26
- ## Contributing
40
+ client = BN::P2P::Client.connect(host)
27
41
 
28
- 1. Fork it ( https://github.com/[my-github-username]/bitcoin_node/fork )
29
- 2. Create your feature branch (`git checkout -b my-new-feature`)
30
- 3. Commit your changes (`git commit -am 'Add some feature'`)
31
- 4. Push to the branch (`git push origin my-new-feature`)
32
- 5. Create a new Pull Request
42
+ client.send(BN::Protocol::Messages.version)
43
+ client.send(BN::Protocol::Messages.ping)
44
+ client.send(BN::Protocol::Messages.getaddr)
45
+ ```
@@ -1,3 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bitcoin_node'
4
+ require 'resolv'
5
+
6
+ SEED = 'seed.bitcoin.sipa.be'
7
+ hosts = Resolv::DNS.new.getaddresses(SEED).map(&:to_s).sample(4)
8
+
9
+ threads = hosts.map do |host|
10
+ Thread.new(host) do |h|
11
+ client = BN::P2p::Client.connect(h)
12
+
13
+ client.send(BN::P::Messages.version)
14
+ client.send(BN::P::Messages.ping)
15
+ end
16
+ end
17
+
18
+ threads.map(&:join)
19
+
20
+ sleep(20)
@@ -18,6 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "celluloid-io"
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.6"
22
24
  spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "pry"
23
27
  end
@@ -0,0 +1,16 @@
1
+ # encoding: ascii-8bit
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
3
+
4
+ require 'bitcoin_node'
5
+
6
+ host = ARGV[0] || (abort 'Missing host')
7
+
8
+ payload = BN::Protocol::Version.new(addr_recv: [host, 8333])
9
+
10
+ message = BN::Protocol::Message.new(payload)
11
+
12
+ client = BN::P2P::Client.connect(host)
13
+ client.send(message)
14
+ client.send(BN::Protocol::Messages.ping)
15
+ client.send(BN::Protocol::Messages.getaddr)
16
+
@@ -1,5 +1,20 @@
1
- require "bitcoin_node/version"
1
+ # coding: ascii-8bit
2
+ require 'logger'
3
+
4
+ require 'bitcoin_node/version'
5
+ require 'bitcoin_node/protocol'
6
+ require 'bitcoin_node/p2p'
2
7
 
3
8
  module BitcoinNode
4
- # Your code goes here...
9
+ NETWORKS = { main: "\xF9\xBE\xB4\xD9".freeze, testnet: "\xFA\xBF\xB5\xDA".freeze }
10
+
11
+ def self.network
12
+ NETWORKS[:main]
13
+ end
14
+
15
+ Logger = ::Logger.new(STDOUT)
16
+ Logger.progname = 'NODE'
5
17
  end
18
+
19
+ BN = BitcoinNode
20
+ BN::P = BN::Protocol
@@ -0,0 +1,10 @@
1
+ module BitcoinNode
2
+ module P2p
3
+ end
4
+
5
+ P2P = P2p
6
+ end
7
+
8
+ require_relative 'p2p/probe'
9
+ require_relative 'p2p/client'
10
+ require_relative 'p2p/server'
@@ -0,0 +1,108 @@
1
+ require 'socket'
2
+
3
+ module BitcoinNode
4
+ module P2p
5
+ class Client
6
+ def self.connect(host, port = 8333, probe = LoggingProbe.new("client-#{host}"))
7
+ new(host, port, probe)
8
+ end
9
+
10
+ attr_accessor :handshaked, :version
11
+
12
+ def initialize(host, port = 8333, probe = LoggingProbe.new("client-#{host}"))
13
+ @host, @buffer, @probe = host, String.new, probe
14
+ @socket = TCPSocket.new(host, port)
15
+ @handshaked = false
16
+ @version = BN::Protocol::VERSION
17
+ @probe << { connected: host }
18
+ end
19
+
20
+ alias_method :handshaked?, :handshaked
21
+
22
+ def send(message)
23
+ raise ArgumentError unless BN::Protocol::Message === message
24
+
25
+ @probe << { sending: message.command }
26
+ @socket.write(message.raw)
27
+
28
+ loop {
29
+ @buffer << @socket.readpartial(1024)
30
+ handler = CommandHandler.new(self, @buffer, @probe)
31
+ if handler.valid_message?
32
+ handler.parse
33
+ break
34
+ end
35
+ }
36
+ rescue IOError => e
37
+ BN::ClientLogger.error(e.message)
38
+ end
39
+
40
+ def close!
41
+ @socket.close
42
+ @probe << { closed: @host }
43
+ end
44
+
45
+ class CommandHandler
46
+ def initialize(client, buffer, probe)
47
+ @client, @buffer, @probe = client, buffer, probe
48
+ end
49
+
50
+ def parse
51
+ @probe << { receiving: @command }
52
+
53
+ callback = Parser.new(@command, @payload).parse
54
+
55
+ @buffer.clear
56
+ callback.call(@client)
57
+ end
58
+
59
+ def valid_message?
60
+ @payload, @command = BN::Protocol::Message.validate(@buffer)
61
+ rescue BN::P::IncompleteMessageError
62
+ false
63
+ rescue BN::P::InvalidChecksumError => e
64
+ BN::Logger.info(e.message)
65
+ false
66
+ end
67
+ end
68
+
69
+ class Parser
70
+ def initialize(command, payload)
71
+ @command, @payload = command, payload
72
+ end
73
+
74
+ def parse
75
+ callback = Proc.new {}
76
+
77
+ if @command == 'version'
78
+ received = BN::Protocol::Version.parse(@payload)
79
+ remote_protocol_version = received.protocol_version.value
80
+ callback = lambda do |client|
81
+ client.version = [remote_protocol_version, client.version].min
82
+ client.send(BN::Protocol::Messages.verack)
83
+ end
84
+ end
85
+
86
+ if @command == 'verack'
87
+ BN::Logger.info('Version handshake finished')
88
+ callback = lambda { |client| client.handshaked = true }
89
+ end
90
+
91
+ if @command == 'addr'
92
+ BN::Logger.info('Parsing addresses')
93
+ BN::Protocol::Addr.parse(@payload)
94
+ end
95
+
96
+ if @command == 'inv'
97
+ BN::Logger.info('Parsing inv')
98
+ p BN::Protocol::Inv.parse(@payload)
99
+ end
100
+
101
+ callback
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,43 @@
1
+ module BitcoinNode
2
+ module P2p
3
+ class NullProbe
4
+ def <<(*args); end
5
+ end
6
+
7
+ class StoreProbe
8
+ attr_reader :store
9
+
10
+ def initialize
11
+ @store = Hash.new { |h, key| h[key] = [] }
12
+ end
13
+
14
+ def <<(hash)
15
+ hash.each do |key, value|
16
+ store[key] << value
17
+ end
18
+ end
19
+ end
20
+
21
+ class LoggingProbe
22
+ attr_reader :logger
23
+
24
+ def initialize(progname)
25
+ @logger = ::Logger.new(STDOUT)
26
+ @logger.progname = progname.upcase
27
+ end
28
+
29
+ def <<(hash)
30
+ hash.each do |key, value|
31
+ case key
32
+ when :sending then logger.info("Sending #{value}")
33
+ when :receiving then logger.info("Receiving #{value}")
34
+ when :connected then logger.info("Connected to #{value}")
35
+ when :connection then logger.info("Connection received from #{value}")
36
+ when :closed then logger.info("Closed connection to #{value}")
37
+ else logger.unknown('Cannot log that!!')
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ require 'socket'
2
+ require 'celluloid/io'
3
+
4
+ module BitcoinNode
5
+ module P2p
6
+ class Server
7
+ include Celluloid::IO
8
+ finalizer :shutdown
9
+
10
+ def initialize(port = 3333, probe = LoggingProbe.new('server'))
11
+ @server = TCPServer.new('localhost', port)
12
+ @probe = probe
13
+ async.run
14
+ end
15
+
16
+ def run
17
+ loop { async.accept_connection @server.accept }
18
+ end
19
+
20
+ def shutdown
21
+ @server.close if @server
22
+ end
23
+
24
+ private
25
+
26
+ def accept_connection(socket)
27
+ _, port, host = socket.peeraddr
28
+ @probe << { connection: "#{host}:#{port}" }
29
+
30
+ loop { handle_messages(socket) }
31
+ end
32
+
33
+ def handle_messages(socket)
34
+ _, port, host = socket.peeraddr
35
+ response = socket.readpartial(1024)
36
+ network, command, length, checksum = response.unpack('a4A12Va4')
37
+ payload = response[24...(24 + length)]
38
+ @probe << { receiving: command }
39
+
40
+ if command == 'version'
41
+ payload = BN::Protocol::Version.new(addr_recv: ['127.0.0.1', port])
42
+ message = BN::Protocol::Message.new(payload)
43
+ @probe << { sending: 'version' }
44
+ socket.write(message.raw)
45
+ end
46
+
47
+ if command == 'verack'
48
+ verack = BN::Protocol::Messages.verack
49
+ @probe << { sending: 'verack' }
50
+ socket.write(verack.raw)
51
+ end
52
+
53
+ if command == 'ping'
54
+ ping_nonce = BN::Protocol::Ping.parse(payload).nonce.value
55
+ pong = BN::Protocol::Messages.pong(ping_nonce)
56
+ @probe << { sending: 'pong' }
57
+ socket.write(pong.raw)
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,196 @@
1
+ # coding: ascii-8bit
2
+ require 'digest'
3
+
4
+ module BitcoinNode
5
+ module Protocol
6
+
7
+ VERSION = 70001
8
+
9
+ MessageParsingError = Class.new(StandardError)
10
+ IncompleteMessageError = Class.new(MessageParsingError)
11
+ InvalidChecksumError = Class.new(MessageParsingError)
12
+
13
+ def self.digest(content)
14
+ Digest::SHA256.digest(Digest::SHA256.digest(content))
15
+ end
16
+
17
+ def self.nonce
18
+ rand(0xffffffffffffffff)
19
+ end
20
+
21
+ class Header
22
+ SIZE = 24
23
+
24
+ def self.build_from(payload)
25
+ new(payload)
26
+ end
27
+
28
+ def self.unpack(raw)
29
+ raw.unpack('a4A12Va4')
30
+ end
31
+
32
+ def initialize(payload)
33
+ @payload = payload
34
+ end
35
+ private_class_method :new
36
+
37
+ def raw
38
+ @raw ||= begin
39
+ [BitcoinNode.network,
40
+ @payload.name.ljust(12, "\x00")[0...12],
41
+ [@payload.bytesize].pack("V"),
42
+ BN::Protocol.digest(@payload.raw)[0...4]].join
43
+ end
44
+ end
45
+ end
46
+
47
+ class Message
48
+ attr_reader :command
49
+
50
+ def initialize(payload)
51
+ raise ArgumentError, 'Expected Payload type' unless Payload === payload
52
+ @payload = payload
53
+ @command = payload.name
54
+ end
55
+
56
+ def raw
57
+ @raw ||= begin
58
+ [Header.build_from(@payload).raw, @payload.raw]
59
+ .join
60
+ .force_encoding('binary')
61
+ end
62
+ end
63
+
64
+ def bytesize
65
+ raw.bytesize
66
+ end
67
+
68
+ def self.validate(raw_message)
69
+ network, command, expected_length, checksum = Header.unpack(raw_message)
70
+ raw_payload = raw_message[Header::SIZE...(Header::SIZE + expected_length)]
71
+ if (actual = raw_payload.bytesize) < expected_length
72
+ raise BN::P::IncompleteMessageError.new("Incomplete message (missing #{expected_length - actual} bytes)")
73
+ elsif checksum != BN::Protocol.digest(raw_payload)[0...4]
74
+ raise BN::P::InvalidChecksumError.new("Invalid checksum on command #{command}")
75
+ else
76
+ [raw_payload, command]
77
+ end
78
+ end
79
+ end
80
+
81
+ module Messages
82
+ module_function
83
+
84
+ def ping
85
+ BN::P::Message.new(BN::Protocol::Ping.new)
86
+ end
87
+
88
+ def pong(nonce)
89
+ BN::P::Message.new(BN::Protocol::Pong.new(nonce: nonce))
90
+ end
91
+
92
+ def verack
93
+ BN::P::Message.new(BN::Protocol::Verack.new)
94
+ end
95
+
96
+ def version
97
+ BN::P::Message.new(BN::Protocol::Version.new)
98
+ end
99
+
100
+ def getaddr
101
+ BN::P::Message.new(BN::Protocol::Getaddr.new)
102
+ end
103
+ end
104
+
105
+ class Payload
106
+ class << self
107
+ def field(name, type, options = {})
108
+ define_method(name) do
109
+ instance_fields[name]
110
+ end
111
+
112
+ define_method("#{name}=") do |value|
113
+ if type === value
114
+ instance_fields[name] = value
115
+ else
116
+ instance_fields[name] = type.new(*Array(value))
117
+ end
118
+ end
119
+
120
+ fields[name] = type
121
+ defaults[name] = options[:default] if options[:default]
122
+ end
123
+
124
+ def defaults
125
+ @defaults ||= {}
126
+ end
127
+
128
+ def fields
129
+ @fields ||= {}
130
+ end
131
+
132
+ def field_names
133
+ fields.keys
134
+ end
135
+
136
+ def parse(payload)
137
+ result = fields.inject({}) do |memo, (field_name, type)|
138
+ custom_parse_method = "parse_#{field_name.to_s}"
139
+ parsed, payload = if respond_to?(custom_parse_method)
140
+ public_send(custom_parse_method, payload, memo)
141
+ else
142
+ type.parse(payload)
143
+ end
144
+ memo[field_name] = parsed
145
+ memo
146
+ end
147
+ new(result)
148
+ end
149
+ end
150
+
151
+ def initialize(attributes = {})
152
+ attributes.each do |k,v|
153
+ self.send("#{k}=", v)
154
+ end
155
+ missings = self.class.field_names - attributes.keys
156
+ missings.each do |k|
157
+ d = self.class.defaults[k]
158
+ self.send("#{k}=", Proc === d ? d.call : d)
159
+ end
160
+ end
161
+
162
+ def raw
163
+ @raw ||= begin
164
+ ordered = instance_fields.values_at(*self.class.field_names)
165
+ ordered.map(&:pack).join
166
+ end
167
+ end
168
+
169
+ def bytesize
170
+ raw.bytesize
171
+ end
172
+
173
+ def type
174
+ self.class.name.split('::').last
175
+ end
176
+
177
+ def name
178
+ type.downcase
179
+ end
180
+
181
+ def to_s
182
+ "#<#{type} #{@instance_fields.inspect}>"
183
+ end
184
+ alias_method :inspect, :to_s
185
+
186
+ private
187
+
188
+ def instance_fields
189
+ @instance_fields ||= {}
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ require 'bitcoin_node/protocol/fields'
196
+ require 'bitcoin_node/protocol/payloads'
@@ -0,0 +1,134 @@
1
+ # coding: ascii-8bit
2
+ require 'digest'
3
+
4
+ module BitcoinNode
5
+ module Protocol
6
+
7
+ class FieldStruct < Struct
8
+ def to_s
9
+ type = self.class.name ? self.class.name.split('::').last : ''
10
+ "#<struct #{type} #{values.join(', ')}>"
11
+ end
12
+ alias_method :inspect, :to_s
13
+ end
14
+
15
+ AddressField = FieldStruct.new(:host, :port) do
16
+ def pack
17
+ sockaddr = Socket.pack_sockaddr_in(port, host)
18
+ p, h = sockaddr[2...4], sockaddr[4...8]
19
+ [[1].pack('Q'), "\x00" * 10, "\xFF\xFF", h, p].join
20
+ end
21
+
22
+ def self.parse(address)
23
+ ip, port, remain = address.unpack("x8x12a4na*")
24
+ [new(ip.unpack("C*").join('.'), port), remain]
25
+ end
26
+ end
27
+
28
+ TimedAddressField = FieldStruct.new(:time, :service, :host, :port) do
29
+ def pack
30
+ end
31
+
32
+ def alive?
33
+ (Time.now.tv_sec - 10800) <= time
34
+ end
35
+
36
+ def self.parse(address)
37
+ time, service, ip, port, remain = address.unpack('VQx12a4na*')
38
+ [new(time, service, ip.unpack("C*").join('.'), port), remain]
39
+ end
40
+ end
41
+
42
+ AddressesListField = FieldStruct.new(:count) do
43
+ def self.parse(count, payload)
44
+ Array.new(count) do
45
+ addr, payload = TimedAddressField.parse(payload)
46
+ addr
47
+ end
48
+ end
49
+ end
50
+
51
+ VariableIntegerField = FieldStruct.new(:value) do
52
+ def pack
53
+ if value < 0xfd; [value].pack("C")
54
+ elsif value <= 0xffff; [0xfd, value].pack("Cv")
55
+ elsif value <= 0xffffffff; [0xfe, value].pack("CV")
56
+ elsif value <= 0xffffffffffffffff; [0xff, value].pack("CQ")
57
+ else raise "Cannot pack integer #{value}"
58
+ end
59
+ end
60
+
61
+ def self.parse(raw)
62
+ case raw.unpack("C")[0]
63
+ when 0xfd; raw.unpack("xva*")
64
+ when 0xfe; raw.unpack("xVa*")
65
+ when 0xff; raw.unpack("xQa*")
66
+ else; raw.unpack("Ca*")
67
+ end
68
+ end
69
+ end
70
+
71
+ InventoryVectorField = FieldStruct.new(:type, :object_hash) do
72
+
73
+ TYPES = { ERROR: 0, MSG_TX: 1, MSG_BLOCK: 2, MSG_FILTERED_BLOCK: 3 }
74
+
75
+ def pack
76
+ [TYPES.fetch(type), object_hash].pack('Va32')
77
+ end
78
+
79
+ def self.parse(raw)
80
+ type_int, object_hash, remain = raw.unpack('Va32a*')
81
+ [new(TYPES.invert.fetch(type_int), object_hash), remain]
82
+ end
83
+ end
84
+
85
+ Integer32Field = FieldStruct.new(:value) do
86
+ def pack
87
+ [value].pack('V')
88
+ end
89
+
90
+ def self.parse(raw)
91
+ i, remain = raw.unpack('Va*')
92
+ [new(i), remain]
93
+ end
94
+ end
95
+
96
+ StringField = FieldStruct.new(:value) do
97
+ def pack
98
+ "#{VariableIntegerField.new(value.bytesize).pack}#{value}"
99
+ end
100
+
101
+ def self.parse(raw)
102
+ size, payload = VariableIntegerField.parse(raw)
103
+ if size > 0
104
+ v, payload = payload.unpack("a#{size}a*")
105
+ [StringField.new(v), payload]
106
+ else
107
+ [nil, payload]
108
+ end
109
+ end
110
+ end
111
+
112
+ Integer64Field = FieldStruct.new(:value) do
113
+ def pack
114
+ [value].pack('Q')
115
+ end
116
+
117
+ def self.parse(raw)
118
+ i, remain = raw.unpack('Qa*')
119
+ [new(i), remain]
120
+ end
121
+ end
122
+
123
+ BooleanField = FieldStruct.new(:value) do
124
+ def pack
125
+ (value == true) ? [0xFF].pack("C") : [0x00].pack("C")
126
+ end
127
+
128
+ def self.parse(raw)
129
+ b, remain = raw.unpack('C')
130
+ [BooleanField.new(b == 0 ? false : true), remain]
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,60 @@
1
+ module BitcoinNode
2
+ module Protocol
3
+ class Version < Payload
4
+ field :protocol_version, Integer32Field, default: BitcoinNode::Protocol::VERSION
5
+ field :services, Integer64Field, default: 1
6
+ field :timestamp, Integer64Field, default: lambda { Time.now.tv_sec }
7
+ field :addr_recv, AddressField, default: ['127.0.0.1', 3333]
8
+ field :addr_from, AddressField, default: ['127.0.0.1', 8333]
9
+ field :nonce, Integer64Field, default: lambda { BN::Protocol.nonce }
10
+ field :user_agent, StringField, default: "/bitcoin_node:#{BitcoinNode::VERSION}/"
11
+ field :start_height, Integer32Field, default: 0
12
+ field :relay, BooleanField, default: true
13
+
14
+ def self.parse_relay(payload, fields_parsed)
15
+ parsed_version = fields_parsed.fetch(:protocol_version).value
16
+ parsed_version >= BitcoinNode::Protocol::VERSION ? BooleanField.parse(payload) : true
17
+ end
18
+
19
+ end
20
+
21
+ class Addr < Payload
22
+ field :count, VariableIntegerField
23
+ field :addr_list, AddressesListField
24
+
25
+ def self.parse(raw)
26
+ count, payload = VariableIntegerField.parse(raw)
27
+ AddressesListField.parse(count, payload)
28
+ end
29
+ end
30
+
31
+ class Inv < Payload
32
+ field :count, VariableIntegerField
33
+ field :inventory_list, InventoryVectorField
34
+
35
+ def self.parse(raw)
36
+ count, payload = VariableIntegerField.parse(raw)
37
+ invs = Array.new(count) do
38
+ inv, payload = InventoryVectorField.parse(payload)
39
+ inv
40
+ end
41
+ new(count: count, inventory_list: invs)
42
+ end
43
+ end
44
+
45
+ class Verack < Payload; end
46
+
47
+ class Getaddr < Payload; end
48
+
49
+ class Ping < Payload
50
+ field :nonce, Integer64Field, default: lambda { BN::Protocol.nonce }
51
+ end
52
+
53
+ class Pong < Payload
54
+ field :nonce, Integer64Field
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+
@@ -1,3 +1,3 @@
1
1
  module BitcoinNode
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Protocol messages' do
4
+
5
+ it 'sends and parses version correctly' do
6
+ now = Time.now.tv_sec
7
+
8
+ version = BN::Protocol::Version.new(
9
+ timestamp: now, addr_recv: ['127.0.0.0', 8333],
10
+ addr_from: ['192.168.0.1', 45555],
11
+ start_height: 127953,
12
+ relay: false,
13
+ )
14
+
15
+ parsed_version = BN::Protocol::Version.parse(version.raw)
16
+
17
+ expect(parsed_version.protocol_version.value).to eql 70001
18
+ expect(parsed_version.services.value).to eql 1
19
+ expect(parsed_version.timestamp).to eql(version.timestamp)
20
+ expect(parsed_version.addr_recv).to eql(version.addr_recv)
21
+ expect(parsed_version.addr_from).to eql(version.addr_from)
22
+ expect(parsed_version.nonce).to eql(version.nonce)
23
+ expect(parsed_version.user_agent.value).to eql("/bitcoin_node:#{BitcoinNode::VERSION}/")
24
+ expect(parsed_version.start_height).to eql(version.start_height)
25
+ expect(parsed_version.relay).to eql(version.relay)
26
+ end
27
+
28
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Version handshake' do
4
+
5
+ let(:port) { 3333 }
6
+
7
+ it 'peers exchanges version properly' do
8
+ server = BN::P2p::Server.new
9
+
10
+ client_probe = BN::P2p::StoreProbe.new
11
+ client = BN::P2p::Client.connect('localhost', port, client_probe)
12
+ client.version = 60001
13
+
14
+ payload = BN::Protocol::Version.new(addr_recv: ['127.0.0.1', port])
15
+ message = BN::Protocol::Message.new(payload)
16
+
17
+ expect(client.handshaked?).to eql false
18
+
19
+ client.send(message)
20
+
21
+ expect(client.handshaked?).to eql true
22
+ expect(client.version).to eql 60001
23
+
24
+ client.send(BN::Protocol::Messages.ping)
25
+
26
+ expect(client_probe.store[:sending]).to eql %w(version verack ping)
27
+ expect(client_probe.store[:receiving]).to eql %w(version verack pong)
28
+ end
29
+
30
+ end
@@ -0,0 +1,4 @@
1
+ require File.expand_path('../lib/bitcoin_node.rb', __dir__)
2
+
3
+ require 'pry'
4
+
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitcoin_node
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - simcap
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-27 00:00:00.000000000 Z
11
+ date: 2014-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: celluloid-io
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,34 @@ dependencies:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
41
83
  description: Simple node on the p2p bitcoin network
42
84
  email:
43
85
  - simcap@fastmail.com
@@ -53,8 +95,19 @@ files:
53
95
  - Rakefile
54
96
  - bin/bitcoin_node
55
97
  - bitcoin_node.gemspec
98
+ - examples/spike.rb
56
99
  - lib/bitcoin_node.rb
100
+ - lib/bitcoin_node/p2p.rb
101
+ - lib/bitcoin_node/p2p/client.rb
102
+ - lib/bitcoin_node/p2p/probe.rb
103
+ - lib/bitcoin_node/p2p/server.rb
104
+ - lib/bitcoin_node/protocol.rb
105
+ - lib/bitcoin_node/protocol/fields.rb
106
+ - lib/bitcoin_node/protocol/payloads.rb
57
107
  - lib/bitcoin_node/version.rb
108
+ - spec/integration/protocol_spec.rb
109
+ - spec/integration/version_handshake_spec.rb
110
+ - spec/spec_helper.rb
58
111
  homepage: ''
59
112
  licenses:
60
113
  - MIT
@@ -79,4 +132,7 @@ rubygems_version: 2.2.2
79
132
  signing_key:
80
133
  specification_version: 4
81
134
  summary: Simple node on the p2p bitcoin network
82
- test_files: []
135
+ test_files:
136
+ - spec/integration/protocol_spec.rb
137
+ - spec/integration/version_handshake_spec.rb
138
+ - spec/spec_helper.rb