bitcoin_node 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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