bitcoin-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. data/.gitignore +12 -0
  2. data/COPYING +18 -0
  3. data/Gemfile +4 -0
  4. data/README.rdoc +189 -0
  5. data/Rakefile +104 -0
  6. data/bin/bitcoin_dns_seed +130 -0
  7. data/bin/bitcoin_gui +80 -0
  8. data/bin/bitcoin_node +174 -0
  9. data/bin/bitcoin_shell +12 -0
  10. data/bin/bitcoin_wallet +323 -0
  11. data/bitcoin-ruby.gemspec +27 -0
  12. data/concept-examples/blockchain-pow.rb +151 -0
  13. data/doc/CONFIG.rdoc +66 -0
  14. data/doc/EXAMPLES.rdoc +9 -0
  15. data/doc/NODE.rdoc +35 -0
  16. data/doc/STORAGE.rdoc +21 -0
  17. data/doc/WALLET.rdoc +102 -0
  18. data/examples/balance.rb +60 -0
  19. data/examples/bbe_verify_tx.rb +55 -0
  20. data/examples/connect.rb +36 -0
  21. data/examples/relay_tx.rb +22 -0
  22. data/examples/verify_tx.rb +57 -0
  23. data/lib/bitcoin.rb +370 -0
  24. data/lib/bitcoin/builder.rb +266 -0
  25. data/lib/bitcoin/config.rb +56 -0
  26. data/lib/bitcoin/connection.rb +126 -0
  27. data/lib/bitcoin/ffi/openssl.rb +121 -0
  28. data/lib/bitcoin/gui/addr_view.rb +42 -0
  29. data/lib/bitcoin/gui/bitcoin-ruby.png +0 -0
  30. data/lib/bitcoin/gui/bitcoin-ruby.svg +80 -0
  31. data/lib/bitcoin/gui/conn_view.rb +36 -0
  32. data/lib/bitcoin/gui/connection.rb +68 -0
  33. data/lib/bitcoin/gui/em_gtk.rb +28 -0
  34. data/lib/bitcoin/gui/gui.builder +1643 -0
  35. data/lib/bitcoin/gui/gui.rb +290 -0
  36. data/lib/bitcoin/gui/helpers.rb +113 -0
  37. data/lib/bitcoin/gui/tree_view.rb +82 -0
  38. data/lib/bitcoin/gui/tx_view.rb +67 -0
  39. data/lib/bitcoin/key.rb +125 -0
  40. data/lib/bitcoin/logger.rb +65 -0
  41. data/lib/bitcoin/network/command_client.rb +93 -0
  42. data/lib/bitcoin/network/command_handler.rb +179 -0
  43. data/lib/bitcoin/network/connection_handler.rb +274 -0
  44. data/lib/bitcoin/network/node.rb +399 -0
  45. data/lib/bitcoin/protocol.rb +140 -0
  46. data/lib/bitcoin/protocol/address.rb +48 -0
  47. data/lib/bitcoin/protocol/alert.rb +47 -0
  48. data/lib/bitcoin/protocol/block.rb +154 -0
  49. data/lib/bitcoin/protocol/handler.rb +38 -0
  50. data/lib/bitcoin/protocol/parser.rb +148 -0
  51. data/lib/bitcoin/protocol/tx.rb +205 -0
  52. data/lib/bitcoin/protocol/txin.rb +97 -0
  53. data/lib/bitcoin/protocol/txout.rb +73 -0
  54. data/lib/bitcoin/protocol/version.rb +70 -0
  55. data/lib/bitcoin/script.rb +634 -0
  56. data/lib/bitcoin/storage/dummy.rb +164 -0
  57. data/lib/bitcoin/storage/models.rb +133 -0
  58. data/lib/bitcoin/storage/sequel.rb +335 -0
  59. data/lib/bitcoin/storage/sequel_store/sequel_migrations.rb +84 -0
  60. data/lib/bitcoin/storage/storage.rb +243 -0
  61. data/lib/bitcoin/version.rb +3 -0
  62. data/lib/bitcoin/wallet/coinselector.rb +30 -0
  63. data/lib/bitcoin/wallet/keygenerator.rb +75 -0
  64. data/lib/bitcoin/wallet/keystore.rb +203 -0
  65. data/lib/bitcoin/wallet/txdp.rb +116 -0
  66. data/lib/bitcoin/wallet/wallet.rb +243 -0
  67. data/spec/bitcoin/bitcoin_spec.rb +472 -0
  68. data/spec/bitcoin/builder_spec.rb +90 -0
  69. data/spec/bitcoin/fixtures/0d0affb5964abe804ffe85e53f1dbb9f29e406aa3046e2db04fba240e63c7fdd.json +27 -0
  70. data/spec/bitcoin/fixtures/23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63.json +23 -0
  71. data/spec/bitcoin/fixtures/477fff140b363ec2cc51f3a65c0c58eda38f4d41f04a295bbd62babf25e4c590.json +27 -0
  72. data/spec/bitcoin/fixtures/60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1.json +45 -0
  73. data/spec/bitcoin/fixtures/bc179baab547b7d7c1d5d8d6f8b0cc6318eaa4b0dd0a093ad6ac7f5a1cb6b3ba.json +34 -0
  74. data/spec/bitcoin/fixtures/rawblock-0.bin +0 -0
  75. data/spec/bitcoin/fixtures/rawblock-0.json +39 -0
  76. data/spec/bitcoin/fixtures/rawblock-1.bin +0 -0
  77. data/spec/bitcoin/fixtures/rawblock-1.json +39 -0
  78. data/spec/bitcoin/fixtures/rawblock-131025.bin +0 -0
  79. data/spec/bitcoin/fixtures/rawblock-131025.json +5063 -0
  80. data/spec/bitcoin/fixtures/rawblock-170.bin +0 -0
  81. data/spec/bitcoin/fixtures/rawblock-170.json +68 -0
  82. data/spec/bitcoin/fixtures/rawblock-9.bin +0 -0
  83. data/spec/bitcoin/fixtures/rawblock-9.json +39 -0
  84. data/spec/bitcoin/fixtures/rawblock-testnet-26478.bin +0 -0
  85. data/spec/bitcoin/fixtures/rawblock-testnet-26478.json +64 -0
  86. data/spec/bitcoin/fixtures/rawtx-01.bin +0 -0
  87. data/spec/bitcoin/fixtures/rawtx-01.json +27 -0
  88. data/spec/bitcoin/fixtures/rawtx-02.bin +0 -0
  89. data/spec/bitcoin/fixtures/rawtx-02.json +27 -0
  90. data/spec/bitcoin/fixtures/rawtx-03.bin +0 -0
  91. data/spec/bitcoin/fixtures/rawtx-03.json +48 -0
  92. data/spec/bitcoin/fixtures/rawtx-04.json +27 -0
  93. data/spec/bitcoin/fixtures/rawtx-0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9.bin +0 -0
  94. data/spec/bitcoin/fixtures/rawtx-05.json +23 -0
  95. data/spec/bitcoin/fixtures/rawtx-14be6fff8c6014f7c9493b4a6e4a741699173f39d74431b6b844fcb41ebb9984.bin +0 -0
  96. data/spec/bitcoin/fixtures/rawtx-2f4a2717ec8c9f077a87dde6cbe0274d5238793a3f3f492b63c744837285e58a.bin +0 -0
  97. data/spec/bitcoin/fixtures/rawtx-2f4a2717ec8c9f077a87dde6cbe0274d5238793a3f3f492b63c744837285e58a.json +27 -0
  98. data/spec/bitcoin/fixtures/rawtx-406b2b06bcd34d3c8733e6b79f7a394c8a431fbf4ff5ac705c93f4076bb77602.json +23 -0
  99. data/spec/bitcoin/fixtures/rawtx-52250a162c7d03d2e1fbc5ebd1801a88612463314b55102171c5b5d817d2d7b2.bin +0 -0
  100. data/spec/bitcoin/fixtures/rawtx-b5d4e8883533f99e5903ea2cf001a133a322fa6b1370b18a16c57c946a40823d.bin +0 -0
  101. data/spec/bitcoin/fixtures/rawtx-ba1ff5cd66713133c062a871a8adab92416f1e38d17786b2bf56ac5f6ffdfdf5.json +37 -0
  102. data/spec/bitcoin/fixtures/rawtx-c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73.json +24 -0
  103. data/spec/bitcoin/fixtures/rawtx-de35d060663750b3975b7997bde7fb76307cec5b270d12fcd9c4ad98b279c28c.json +23 -0
  104. data/spec/bitcoin/fixtures/rawtx-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16.bin +0 -0
  105. data/spec/bitcoin/fixtures/rawtx-testnet-a220adf1902c46a39db25a24bc4178b6a88440f977a7e2cabfdd8b5c1dd35cfb.json +27 -0
  106. data/spec/bitcoin/fixtures/rawtx-testnet-e232e0055dbdca88bbaa79458683195a0b7c17c5b6c524a8d146721d4d4d652f.bin +0 -0
  107. data/spec/bitcoin/fixtures/rawtx-testnet-e232e0055dbdca88bbaa79458683195a0b7c17c5b6c524a8d146721d4d4d652f.json +41 -0
  108. data/spec/bitcoin/fixtures/reorg/blk_0_to_4.dat +0 -0
  109. data/spec/bitcoin/fixtures/reorg/blk_3A.dat +0 -0
  110. data/spec/bitcoin/fixtures/reorg/blk_4A.dat +0 -0
  111. data/spec/bitcoin/fixtures/reorg/blk_5A.dat +0 -0
  112. data/spec/bitcoin/fixtures/testnet/block_0.bin +0 -0
  113. data/spec/bitcoin/fixtures/testnet/block_1.bin +0 -0
  114. data/spec/bitcoin/fixtures/testnet/block_2.bin +0 -0
  115. data/spec/bitcoin/fixtures/testnet/block_3.bin +0 -0
  116. data/spec/bitcoin/fixtures/testnet/block_4.bin +0 -0
  117. data/spec/bitcoin/fixtures/testnet/block_5.bin +0 -0
  118. data/spec/bitcoin/fixtures/txdp-1.txt +32 -0
  119. data/spec/bitcoin/fixtures/txdp-2-signed.txt +19 -0
  120. data/spec/bitcoin/fixtures/txdp-2-unsigned.txt +14 -0
  121. data/spec/bitcoin/key_spec.rb +123 -0
  122. data/spec/bitcoin/network_spec.rb +48 -0
  123. data/spec/bitcoin/protocol/addr_spec.rb +68 -0
  124. data/spec/bitcoin/protocol/alert_spec.rb +20 -0
  125. data/spec/bitcoin/protocol/block_spec.rb +101 -0
  126. data/spec/bitcoin/protocol/inv_spec.rb +124 -0
  127. data/spec/bitcoin/protocol/ping_spec.rb +49 -0
  128. data/spec/bitcoin/protocol/tx_spec.rb +226 -0
  129. data/spec/bitcoin/protocol/version_spec.rb +77 -0
  130. data/spec/bitcoin/reorg_spec.rb +129 -0
  131. data/spec/bitcoin/script/opcodes_spec.rb +417 -0
  132. data/spec/bitcoin/script/script_spec.rb +246 -0
  133. data/spec/bitcoin/spec_helper.rb +36 -0
  134. data/spec/bitcoin/storage_spec.rb +229 -0
  135. data/spec/bitcoin/wallet/coinselector_spec.rb +35 -0
  136. data/spec/bitcoin/wallet/keygenerator_spec.rb +64 -0
  137. data/spec/bitcoin/wallet/keystore_spec.rb +188 -0
  138. data/spec/bitcoin/wallet/txdp_spec.rb +74 -0
  139. data/spec/bitcoin/wallet/wallet_spec.rb +207 -0
  140. metadata +295 -0
@@ -0,0 +1,274 @@
1
+ require 'eventmachine'
2
+
3
+ module Bitcoin::Network
4
+
5
+ # Node network connection to a peer. Handles all the communication with a specific peer.
6
+ # TODO: incoming/outgoing?
7
+ class ConnectionHandler < EM::Connection
8
+
9
+ include Bitcoin
10
+ include Bitcoin::Storage
11
+
12
+ attr_reader :host, :port, :state, :version
13
+
14
+ def hth(h); h.unpack("H*")[0]; end
15
+ def htb(h); [h].pack("H*"); end
16
+
17
+ def log
18
+ @log ||= Logger::LogWrapper.new("#@host:#@port", @node.log)
19
+ end
20
+
21
+ # how long has this connection been open?
22
+ def uptime
23
+ @started ? (Time.now - @started).to_i : 0
24
+ end
25
+
26
+ # create connection to +host+:+port+ for given +node+
27
+ def initialize node, host, port
28
+ @node, @host, @port = node, host, port
29
+ @parser = Bitcoin::Protocol::Parser.new(self)
30
+ @state = :new
31
+ @version = nil
32
+ @started = nil
33
+ rescue Exception
34
+ log.fatal { "Error in #initialize" }
35
+ p $!; puts $@; exit
36
+ end
37
+
38
+ # check if connection is wanted, begin handshake if it is, disconnect if not
39
+ def post_init
40
+ if @node.connections.size >= @node.config[:max][:connections]
41
+ return close_connection unless @node.config[:connect].include?([@host, @port.to_s])
42
+ end
43
+ log.info { "Connected to #{@host}:#{@port}" }
44
+ @state = :established
45
+ @node.connections << self
46
+ on_handshake_begin
47
+ rescue Exception
48
+ log.fatal { "Error in #post_init" }
49
+ p $!; puts $@; exit
50
+ end
51
+
52
+ # receive data from peer and invoke Protocol::Parser
53
+ def receive_data data
54
+ #log.debug { "Receiving data (#{data.size} bytes)" }
55
+ @parser.parse(data)
56
+ end
57
+
58
+ # connection closed; notify listeners and cleanup connection from node
59
+ def unbind
60
+ log.info { "Disconnected #{@host}:#{@port}" }
61
+ @node.notifiers[:connection].push([:disconnected, [@host, @port]])
62
+ @state = :disconnected
63
+ @node.connections.delete(self)
64
+ end
65
+
66
+ # received +inv_tx+ message for given +hash+.
67
+ # add to inv_queue, unlesss maximum is reached
68
+ def on_inv_transaction(hash)
69
+ log.debug { ">> inv transaction: #{hth(hash)}" }
70
+ return if @node.inv_queue.size >= @node.config[:max][:inv]
71
+ @node.queue_inv([:tx, hash, self])
72
+ end
73
+
74
+ # received +inv_block+ message for given +hash+.
75
+ # add to inv_queue, unless maximum is reached
76
+ def on_inv_block(hash)
77
+ log.debug { ">> inv block: #{hth(hash)}" }
78
+ return if @node.inv_queue.size >= @node.config[:max][:inv]
79
+ @node.queue_inv([:block, hash, self])
80
+ end
81
+
82
+ # received +get_tx+ message for given +hash+.
83
+ # send specified tx if we have it
84
+ def on_get_transaction(hash)
85
+ log.debug { ">> get transaction: #{hash.unpack("H*")[0]}" }
86
+ tx = @node.store.get_tx(hash.unpack("H*")[0])
87
+ return unless tx
88
+ pkt = Bitcoin::Protocol.pkt("tx", tx.to_payload)
89
+ log.debug { "<< tx: #{tx.hash}" }
90
+ send_data pkt
91
+ end
92
+
93
+ # received +get_block+ message for given +hash+.
94
+ # send specified block if we have it
95
+ # TODO
96
+ def on_get_block(hash)
97
+ log.debug { ">> get block: #{hth(hash)}" }
98
+ end
99
+
100
+ # send +inv+ message with given +type+ for given +obj+
101
+ def send_inv type, obj
102
+ pkt = Protocol.inv_pkt(type, [[obj.hash].pack("H*")])
103
+ log.debug { "<< inv #{type}: #{obj.hash}" }
104
+ send_data(pkt)
105
+ end
106
+
107
+ # received +addr+ message for given +addr+.
108
+ # store addr in node and notify listeners
109
+ def on_addr(addr)
110
+ log.debug { ">> addr: #{addr.ip}:#{addr.port} alive: #{addr.alive?}, service: #{addr.service}" }
111
+ @node.addrs << addr
112
+ @node.notifiers[:addr].push(addr)
113
+ end
114
+
115
+ # received +tx+ message for given +tx+.
116
+ # push tx to storage queue
117
+ def on_tx(tx)
118
+ log.debug { ">> tx: #{tx.hash} (#{tx.payload.size} bytes)" }
119
+ @node.queue.push([:tx, tx])
120
+ end
121
+
122
+ # received +block+ message for given +blk+.
123
+ # push block to storage queue
124
+ def on_block(blk)
125
+ log.debug { ">> block: #{blk.hash} (#{blk.payload.size} bytes)" }
126
+ @node.queue.push([:block, blk])
127
+ end
128
+
129
+ # received +headers+ message for given +headers+.
130
+ # push each header to storage queue
131
+ def on_headers(headers)
132
+ log.info { ">> headers (#{headers.size})" }
133
+ headers.each {|h| @node.queue.push([:block, h])}
134
+ end
135
+
136
+ # received +version+ message for given +version+.
137
+ # send +verack+ message and complete handshake
138
+ def on_version(version)
139
+ log.info { ">> version: #{version.version}" }
140
+ @version = version
141
+ log.info { "<< verack" }
142
+ send_data( Protocol.verack_pkt )
143
+ on_handshake_complete
144
+ end
145
+
146
+ # received +verack+ message.
147
+ # complete handshake if it isn't completed already
148
+ def on_verack
149
+ log.info { ">> verack" }
150
+ on_handshake_complete if handshake?
151
+ end
152
+
153
+ # received +alert+ message for given +alert+.
154
+ # TODO: implement alert logic, store, display, relay
155
+ def on_alert(alert)
156
+ log.warn { ">> alert: #{alert.inspect}" }
157
+ end
158
+
159
+ # send +getdata tx+ message for given tx +hash+
160
+ def send_getdata_tx(hash)
161
+ pkt = Protocol.getdata_pkt(:tx, [hash])
162
+ log.debug { "<< getdata tx: #{hth(hash)}" }
163
+ send_data(pkt)
164
+ end
165
+
166
+ # send +getdata block+ message for given block +hash+
167
+ def send_getdata_block(hash)
168
+ pkt = Protocol.getdata_pkt(:block, [hash])
169
+ log.debug { "<< getdata block: #{hth(hash)}" }
170
+ send_data(pkt)
171
+ end
172
+
173
+ # send +getblocks+ message
174
+ def send_getblocks locator = @node.store.get_locator
175
+ return get_genesis_block if @node.store.get_depth == -1
176
+ pkt = Protocol.pkt("getblocks", [Bitcoin::network[:magic_head],
177
+ locator.size.chr, *locator.map{|l| htb(l).reverse}, "\x00"*32].join)
178
+ log.info { "<< getblocks: #{locator.first}" }
179
+ send_data(pkt)
180
+ end
181
+
182
+ # send +getheaders+ message
183
+ def send_getheaders locator = @node.store.get_locator
184
+ return get_genesis_block if @node.store.get_depth == -1
185
+ pkt = Protocol.pkt("getheaders", [Bitcoin::network[:magic_head],
186
+ locator.size.chr, *locator.map{|l| htb(l).reverse}, "\x00"*32].join)
187
+ log.debug { "<< getheaders: #{locator.first}" }
188
+ send_data(pkt)
189
+ end
190
+
191
+ # send +getaddr+ message
192
+ def send_getaddr
193
+ log.debug { "<< getaddr" }
194
+ send_data(Protocol.pkt("getaddr", ""))
195
+ end
196
+
197
+ # send +ping+ message
198
+ # TODO: wait for pong and disconnect if it doesn't arrive (and version is new enough)
199
+ def send_ping
200
+ nonce = rand(0xffffffff)
201
+ log.debug { "<< ping (#{nonce})" }
202
+ send_data(Protocol.ping_pkt(nonce))
203
+ end
204
+
205
+ # ask for the genesis block
206
+ def get_genesis_block
207
+ log.info { "Asking for genesis block" }
208
+ pkt = Protocol.getdata_pkt(:block, [htb(Bitcoin::network[:genesis_hash])])
209
+ send_data(pkt)
210
+ end
211
+
212
+ # complete handshake; set state, started time, notify listeners and add address to Node
213
+ def on_handshake_complete
214
+ return unless handshake?
215
+ log.debug { "handshake complete" }
216
+ @state = :connected
217
+ @started = Time.now
218
+ @node.notifiers[:connection].push([:connected, info])
219
+ @node.addrs << addr
220
+ # send_getaddr
221
+ # EM.add_periodic_timer(15) { send_ping }
222
+ end
223
+
224
+ # received +ping+ message with given +nonce+.
225
+ # send +pong+ message back, if +nonce+ is set.
226
+ # network versions <=60000 don't set the nonce and don't expect a pong.
227
+ def on_ping nonce
228
+ log.debug { ">> ping (#{nonce})" }
229
+ send_data(Protocol.pong_pkt(nonce)) if nonce
230
+ end
231
+
232
+ # received +pong+ message with given +nonce+.
233
+ # TODO: see #send_ping
234
+ def on_pong nonce
235
+ log.debug { ">> pong (#{nonce})" }
236
+ end
237
+
238
+ # begin handshake; send +version+ message
239
+ def on_handshake_begin
240
+ @state = :handshake
241
+ block = @node.store.get_depth
242
+ from = "127.0.0.1:8333"
243
+ from_id = Bitcoin::Protocol::Uniq
244
+ to = @node.config[:listen].join(':')
245
+
246
+ pkt = Protocol.version_pkt(from_id, from, to, block)
247
+ log.info { "<< version (#{Bitcoin::Protocol::VERSION})" }
248
+ send_data(pkt)
249
+ end
250
+
251
+ # get Addr object for this connection
252
+ def addr
253
+ return @addr if @addr
254
+ @addr = Bitcoin::Protocol::Addr.new
255
+ @addr.time, @addr.service, @addr.ip, @addr.port =
256
+ Time.now.tv_sec, @version.services, @host, @port
257
+ @addr
258
+ end
259
+
260
+ [:new, :handshake, :connected].each do |state|
261
+ define_method("#{state}?") { @state == state }
262
+ end
263
+
264
+ # get info hash
265
+ def info
266
+ {
267
+ :host => @host, :port => @port, :state => @state,
268
+ :version => @version.version, :block => @version.block, :started => @started.to_i,
269
+ :user_agent => @version.user_agent
270
+ }
271
+ end
272
+ end
273
+
274
+ end
@@ -0,0 +1,399 @@
1
+ Bitcoin.require_dependency :eventmachine
2
+ Bitcoin.require_dependency :json
3
+ require 'fileutils'
4
+
5
+ module Bitcoin::Network
6
+
7
+ class Node
8
+
9
+ # configuration hash
10
+ attr_reader :config
11
+
12
+ # logger
13
+ attr_reader :log
14
+
15
+ # connections to other peers (Array of ConnectionHandler)
16
+ attr_reader :connections
17
+
18
+ # command connections (Array of CommandHandler)
19
+ attr_reader :command_connections
20
+
21
+ # storage queue (blocks/tx waiting to be stored)
22
+ attr_reader :queue
23
+
24
+ # inventory queue (blocks/tx waiting to be downloaded)
25
+ attr_reader :inv_queue
26
+
27
+ # inventory cache (blocks/tx recently downloaded)
28
+ attr_reader :inv_cache
29
+
30
+ # Bitcoin::Storage backend
31
+ attr_reader :store
32
+
33
+ # peer addrs (Array of Bitcoin::Protocol::Addr)
34
+ attr_reader :addrs
35
+
36
+ # clients to be notified for new block/tx events
37
+ attr_reader :notifiers
38
+
39
+ attr_reader :in_sync
40
+
41
+ DEFAULT_CONFIG = {
42
+ :listen => ["0.0.0.0", Bitcoin.network[:default_port]],
43
+ :connect => [],
44
+ :command => "",
45
+ :storage => Bitcoin::Storage.dummy({}),
46
+ :headers_only => false,
47
+ :dns => true,
48
+ :epoll => false,
49
+ :epoll_limit => 10000,
50
+ :epoll_user => nil,
51
+ :addr_file => "#{ENV['HOME']}/.bitcoin-ruby/addrs.json",
52
+ :log => {
53
+ :network => :info,
54
+ :storage => :info,
55
+ },
56
+ :max => {
57
+ :connections => 8,
58
+ :addr => 256,
59
+ :queue => 64,
60
+ :inv => 128,
61
+ :inv_cache => 1024,
62
+ },
63
+ :intervals => {
64
+ :queue => 5,
65
+ :inv_queue => 5,
66
+ :addrs => 5,
67
+ :connect => 15,
68
+ :relay => 600,
69
+ },
70
+ }
71
+
72
+ def initialize config = {}
73
+ @config = DEFAULT_CONFIG.deep_merge(config)
74
+ @log = Bitcoin::Logger.create(:network, @config[:log][:network])
75
+ @connections = []
76
+ @command_connections = []
77
+ @queue = []
78
+ @queue_thread = nil
79
+ @inv_queue = []
80
+ @inv_queue_thread = nil
81
+ set_store
82
+ load_addrs
83
+ @timers = {}
84
+ @inv_cache = []
85
+ @notifiers = Hash[[:block, :tx, :connection, :addr].map {|n| [n, EM::Channel.new]}]
86
+ @in_sync = false
87
+ end
88
+
89
+ def set_store
90
+ backend, config = @config[:storage].split('::')
91
+ @store = Bitcoin::Storage.send(backend, {:db => config}, ->(locator) {
92
+ peer = @connections.select(&:connected?).sample
93
+ peer.send_getblocks(locator)
94
+ })
95
+ @store.log.level = @config[:log][:storage]
96
+ end
97
+
98
+ def load_addrs
99
+ unless File.exist?(@config[:addr_file])
100
+ @addrs = []
101
+ return
102
+ end
103
+ @addrs = JSON.load(File.read(@config[:addr_file])).map do |a|
104
+ addr = Bitcoin::P::Addr.new
105
+ addr.time, addr.service, addr.ip, addr.port =
106
+ a['time'], a['service'], a['ip'], a['port']
107
+ addr
108
+ end
109
+ log.info { "Initialized #{@addrs.size} addrs from #{@config[:addr_file]}." }
110
+ end
111
+
112
+ def store_addrs
113
+ return if !@addrs || !@addrs.any?
114
+ file = @config[:addr_file]
115
+ FileUtils.mkdir_p(File.dirname(file))
116
+ File.open(file, 'w') do |f|
117
+ addrs = @addrs.map {|a|
118
+ Hash[[:time, :service, :ip, :port].zip(a.entries)] rescue nil }.compact
119
+ f.write(JSON.pretty_generate(addrs))
120
+ end
121
+ log.info { "Stored #{@addrs.size} addrs to #{file}" }
122
+ rescue
123
+ log.warn { "Error storing addrs to #{file}." }
124
+ end
125
+
126
+ def stop
127
+ log.info { "Shutting down..." }
128
+ EM.stop
129
+ end
130
+
131
+ def uptime
132
+ (Time.now - @started).to_i
133
+ end
134
+
135
+ def run
136
+ @started = Time.now
137
+
138
+ EM.add_shutdown_hook do
139
+ store_addrs
140
+ log.info { "Bye" }
141
+ end
142
+
143
+ init_epoll if @config[:epoll]
144
+
145
+ EM.run do
146
+ [:addrs, :connect, :relay].each do |name|
147
+ interval = @config[:intervals][name]
148
+ next if !interval || interval == 0
149
+ @timers[name] = EM.add_periodic_timer(interval, method("work_#{name}"))
150
+ end
151
+
152
+ if @config[:command]
153
+ host, port = @config[:command]
154
+ EM.start_server(host, port, CommandHandler, self)
155
+ log.info { "Command socket listening on #{host}:#{port}" }
156
+ end
157
+
158
+ if @config[:listen]
159
+ host, port = @config[:listen]
160
+ EM.start_server(host, port.to_i, ConnectionHandler, self, host, port.to_i)
161
+ log.info { "Server socket listening on #{host}:#{port}" }
162
+ end
163
+
164
+ if @config[:connect].any?
165
+ @config[:connect].each{|host| connect_peer(*host) }
166
+ end
167
+
168
+ work_connect if @addrs.any?
169
+ connect_dns if @config[:dns]
170
+ work_inv_queue
171
+ work_queue
172
+ end
173
+ end
174
+
175
+ # connect to peer at given +host+ / +port+
176
+ def connect_peer host, port
177
+ return if @connections.map{|c| c.host}.include?(host)
178
+ log.info { "Attempting to connect to #{host}:#{port}" }
179
+ EM.connect(host, port.to_i, ConnectionHandler, self, host, port.to_i)
180
+ rescue
181
+ log.warn { "Error connecting to #{host}:#{port}" }
182
+ log.debug { $!.inspect }
183
+ end
184
+
185
+ # query addrs from dns seed and connect
186
+ def connect_dns
187
+ unless Bitcoin.network[:dns_seeds].any?
188
+ return log.warn { "No DNS seed nodes available" }
189
+ end
190
+ connect_dns_resolver(Bitcoin.network[:dns_seeds].sample) do |addrs|
191
+ log.debug { "DNS returned addrs: #{addrs.inspect}" }
192
+ addrs.sample(@config[:max][:connections] / 2).uniq.each do |addr|
193
+ connect_peer(addr, Bitcoin.network[:default_port])
194
+ end
195
+ end
196
+ end
197
+
198
+ # get peer addrs from given dns +seed+ using em/dns_resolver.
199
+ # fallback to using `nslookup` if it is not installed or fails.
200
+ def connect_dns_resolver(seed)
201
+ if Bitcoin.require_dependency "em/dns_resolver", gem: "em-dns", exit: false
202
+ log.info { "Querying addresses from DNS seed: #{seed}" }
203
+
204
+ dns = EM::DnsResolver.resolve(seed)
205
+ dns.callback {|addrs| yield(addrs) }
206
+ dns.errback do |*a|
207
+ log.error { "Cannot resolve DNS seed #{seed}: #{a.inspect}" }
208
+ connect_dns_nslookup(Bitcoin.network[:dns_seeds].sample) {|a| yield(a) }
209
+ end
210
+ else
211
+ log.info { "Falling back to nslookup resolver." }
212
+ connect_dns_nslookup(seed) {|a| yield(a) }
213
+ end
214
+ end
215
+
216
+ # get peers from dns via nslookup
217
+ def connect_dns_nslookup(seed)
218
+ log.info { "Querying addresses from DNS seed: #{seed}" }
219
+ addrs = `nslookup #{seed}`.scan(/Address\: (.+)$/).flatten
220
+ # exit if @config[:dns] && hosts.size == 0
221
+ yield(addrs)
222
+ end
223
+
224
+ # check if there are enough connections and try to
225
+ # establish new ones if needed
226
+ def work_connect
227
+ log.debug { "Connect worker running" }
228
+ desired = @config[:max][:connections] - @connections.size
229
+ return if desired <= 0
230
+ desired = 32 if desired > 32 # connect to max 32 peers at once
231
+ if addrs.any?
232
+ addrs.sample(desired) do |addr|
233
+ Time.now.tv_sec + 10800 - addr.time
234
+ end.each do |addr|
235
+ connect_peer(addr.ip, addr.port)
236
+ end
237
+ elsif @config[:dns]
238
+ connect_dns
239
+ end
240
+ rescue
241
+ log.error { "Error during connect: #{$!.inspect}" }
242
+ end
243
+
244
+ # query blocks from random peer
245
+ def getblocks locator = store.get_locator
246
+ peer = @connections.select(&:connected?).sample
247
+ return unless peer
248
+ log.info { "querying blocks from #{peer.host}:#{peer.port}" }
249
+ if @config[:headers_only]
250
+ peer.send_getheaders locator unless @queue.size >= @config[:max][:queue]
251
+ else
252
+ peer.send_getblocks locator unless @inv_queue.size >= @config[:max][:inv]
253
+ end
254
+ end
255
+
256
+ # check if the addr store is full and request new addrs
257
+ # from a random peer if it isn't
258
+ def work_addrs
259
+ log.debug { "addr worker running" }
260
+ @addrs.delete_if{|addr| !addr.alive? } if @addrs.size >= @config[:max][:addr]
261
+ return if !@connections.any? || @config[:max][:connections] <= @connections.size
262
+ connections = @connections.select(&:connected?)
263
+ return unless connections.any?
264
+ log.info { "requesting addrs" }
265
+ connections.sample.send_getaddr
266
+ end
267
+
268
+ # check for new items in the queue and process them
269
+ def work_queue
270
+ @log.debug { "queue worker running" }
271
+ EM.defer(nil, proc { work_queue }) do
272
+ if @queue.size == 0
273
+ getblocks if @inv_queue.size == 0 && !@in_sync
274
+ sleep @config[:intervals][:queue]
275
+ end
276
+ while obj = @queue.shift
277
+ begin
278
+ if @store.send("store_#{obj[0]}", obj[1])
279
+ if obj[0].to_sym == :block
280
+ block = @store.get_block(obj[1].hash)
281
+ @notifiers[:block].push([obj[1], block.depth]) if block.chain == 0
282
+ else
283
+ @notifiers[:tx].push([obj[1]])
284
+ end
285
+ end
286
+ rescue
287
+ @log.warn { $!.inspect }
288
+ puts *$@
289
+ end
290
+ end
291
+ @in_sync = (@store.get_head && (Time.now - @store.get_head.time).to_i < 3600) ? true : false
292
+ end
293
+ end
294
+
295
+ # check for new items in the inv queue and process them,
296
+ # unless the queue is already full
297
+ def work_inv_queue
298
+ EM.defer(nil, proc { work_inv_queue }) do
299
+ sleep @config[:intervals][:inv_queue] if @inv_queue.size == 0
300
+ @log.debug { "inv queue worker running" }
301
+ if @queue.size >= @config[:max][:queue]
302
+ sleep @config[:intervals][:inv_queue]
303
+ else
304
+ while inv = @inv_queue.shift
305
+ next if !@in_sync && inv[0] == :tx
306
+ next if @queue.map{|i|i[1]}.map(&:hash).include?(inv[1])
307
+ # next if @store.send("has_#{inv[0]}", inv[1])
308
+ inv[2].send("send_getdata_#{inv[0]}", inv[1])
309
+ end
310
+ end
311
+ end
312
+ end
313
+
314
+ # queue inv, caching the most current ones
315
+ def queue_inv inv
316
+ @inv_cache.shift(128) if @inv_cache.size > @config[:max][:inv_cache]
317
+ return if @inv_cache.include?([inv[0], inv[1]]) ||
318
+ @inv_queue.size >= @config[:max][:inv] ||
319
+ (!@in_sync && inv[0] == :tx)
320
+ @inv_cache << [inv[0], inv[1]]
321
+ @inv_queue << inv
322
+ end
323
+
324
+
325
+ # initiate epoll with given file descriptor and set effective user
326
+ def init_epoll
327
+ log.info { "EPOLL: Available file descriptors: " +
328
+ EM.set_descriptor_table_size(@config[:epoll_limit]).to_s }
329
+ if @config[:epoll_user]
330
+ EM.set_effective_user(@config[:epoll_user])
331
+ log.info { "EPOLL: Effective user set to: #{@config[:epoll_user]}" }
332
+ end
333
+ EM.epoll
334
+ end
335
+
336
+ def relay_tx(tx)
337
+ return false unless @in_sync
338
+ @store.store_tx(tx)
339
+ @connections.select(&:connected?).sample((@connections.size / 2) + 1).each do |peer|
340
+ peer.send_inv(:tx, tx)
341
+ end
342
+ end
343
+
344
+ def work_relay
345
+ log.debug { "relay worker running" }
346
+ @store.get_unconfirmed_tx.each do |tx|
347
+ log.info { "relaying tx #{tx.hash}" }
348
+ relay_tx(tx)
349
+ end
350
+ end
351
+
352
+ end
353
+ end
354
+
355
+ class Array
356
+ def random(weights=nil)
357
+ return random(map {|n| yield(n) }) if block_given?
358
+ return random(map {|n| n.send(weights) }) if weights.is_a? Symbol
359
+
360
+ weights ||= Array.new(length, 1.0)
361
+ total = weights.inject(0.0) {|t,w| t+w}
362
+ point = rand * total
363
+
364
+ zip(weights).each do |n,w|
365
+ return n if w >= point
366
+ point -= w
367
+ end
368
+ end
369
+
370
+ def weighted_sample(n, weights = nil)
371
+ src = dup
372
+ buf = []
373
+ n = src.size if n > src.size
374
+ while buf.size < n
375
+ if block_given?
376
+ item = src.random {|n| yield(n) }
377
+ else
378
+ item = src.random(weights)
379
+ end
380
+ buf << item; src.delete(item)
381
+ end
382
+ buf
383
+ end
384
+
385
+ class ::Hash
386
+ def deep_merge(hash)
387
+ target = dup
388
+ hash.keys.each do |key|
389
+ if hash[key].is_a? Hash and self[key].is_a? Hash
390
+ target[key] = target[key].deep_merge(hash[key])
391
+ next
392
+ end
393
+ target[key] = hash[key]
394
+ end
395
+ target
396
+ end
397
+ end
398
+
399
+ end