bitcoin-ruby 0.0.1

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.
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