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.
- data/.gitignore +12 -0
- data/COPYING +18 -0
- data/Gemfile +4 -0
- data/README.rdoc +189 -0
- data/Rakefile +104 -0
- data/bin/bitcoin_dns_seed +130 -0
- data/bin/bitcoin_gui +80 -0
- data/bin/bitcoin_node +174 -0
- data/bin/bitcoin_shell +12 -0
- data/bin/bitcoin_wallet +323 -0
- data/bitcoin-ruby.gemspec +27 -0
- data/concept-examples/blockchain-pow.rb +151 -0
- data/doc/CONFIG.rdoc +66 -0
- data/doc/EXAMPLES.rdoc +9 -0
- data/doc/NODE.rdoc +35 -0
- data/doc/STORAGE.rdoc +21 -0
- data/doc/WALLET.rdoc +102 -0
- data/examples/balance.rb +60 -0
- data/examples/bbe_verify_tx.rb +55 -0
- data/examples/connect.rb +36 -0
- data/examples/relay_tx.rb +22 -0
- data/examples/verify_tx.rb +57 -0
- data/lib/bitcoin.rb +370 -0
- data/lib/bitcoin/builder.rb +266 -0
- data/lib/bitcoin/config.rb +56 -0
- data/lib/bitcoin/connection.rb +126 -0
- data/lib/bitcoin/ffi/openssl.rb +121 -0
- data/lib/bitcoin/gui/addr_view.rb +42 -0
- data/lib/bitcoin/gui/bitcoin-ruby.png +0 -0
- data/lib/bitcoin/gui/bitcoin-ruby.svg +80 -0
- data/lib/bitcoin/gui/conn_view.rb +36 -0
- data/lib/bitcoin/gui/connection.rb +68 -0
- data/lib/bitcoin/gui/em_gtk.rb +28 -0
- data/lib/bitcoin/gui/gui.builder +1643 -0
- data/lib/bitcoin/gui/gui.rb +290 -0
- data/lib/bitcoin/gui/helpers.rb +113 -0
- data/lib/bitcoin/gui/tree_view.rb +82 -0
- data/lib/bitcoin/gui/tx_view.rb +67 -0
- data/lib/bitcoin/key.rb +125 -0
- data/lib/bitcoin/logger.rb +65 -0
- data/lib/bitcoin/network/command_client.rb +93 -0
- data/lib/bitcoin/network/command_handler.rb +179 -0
- data/lib/bitcoin/network/connection_handler.rb +274 -0
- data/lib/bitcoin/network/node.rb +399 -0
- data/lib/bitcoin/protocol.rb +140 -0
- data/lib/bitcoin/protocol/address.rb +48 -0
- data/lib/bitcoin/protocol/alert.rb +47 -0
- data/lib/bitcoin/protocol/block.rb +154 -0
- data/lib/bitcoin/protocol/handler.rb +38 -0
- data/lib/bitcoin/protocol/parser.rb +148 -0
- data/lib/bitcoin/protocol/tx.rb +205 -0
- data/lib/bitcoin/protocol/txin.rb +97 -0
- data/lib/bitcoin/protocol/txout.rb +73 -0
- data/lib/bitcoin/protocol/version.rb +70 -0
- data/lib/bitcoin/script.rb +634 -0
- data/lib/bitcoin/storage/dummy.rb +164 -0
- data/lib/bitcoin/storage/models.rb +133 -0
- data/lib/bitcoin/storage/sequel.rb +335 -0
- data/lib/bitcoin/storage/sequel_store/sequel_migrations.rb +84 -0
- data/lib/bitcoin/storage/storage.rb +243 -0
- data/lib/bitcoin/version.rb +3 -0
- data/lib/bitcoin/wallet/coinselector.rb +30 -0
- data/lib/bitcoin/wallet/keygenerator.rb +75 -0
- data/lib/bitcoin/wallet/keystore.rb +203 -0
- data/lib/bitcoin/wallet/txdp.rb +116 -0
- data/lib/bitcoin/wallet/wallet.rb +243 -0
- data/spec/bitcoin/bitcoin_spec.rb +472 -0
- data/spec/bitcoin/builder_spec.rb +90 -0
- data/spec/bitcoin/fixtures/0d0affb5964abe804ffe85e53f1dbb9f29e406aa3046e2db04fba240e63c7fdd.json +27 -0
- data/spec/bitcoin/fixtures/23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63.json +23 -0
- data/spec/bitcoin/fixtures/477fff140b363ec2cc51f3a65c0c58eda38f4d41f04a295bbd62babf25e4c590.json +27 -0
- data/spec/bitcoin/fixtures/60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1.json +45 -0
- data/spec/bitcoin/fixtures/bc179baab547b7d7c1d5d8d6f8b0cc6318eaa4b0dd0a093ad6ac7f5a1cb6b3ba.json +34 -0
- data/spec/bitcoin/fixtures/rawblock-0.bin +0 -0
- data/spec/bitcoin/fixtures/rawblock-0.json +39 -0
- data/spec/bitcoin/fixtures/rawblock-1.bin +0 -0
- data/spec/bitcoin/fixtures/rawblock-1.json +39 -0
- data/spec/bitcoin/fixtures/rawblock-131025.bin +0 -0
- data/spec/bitcoin/fixtures/rawblock-131025.json +5063 -0
- data/spec/bitcoin/fixtures/rawblock-170.bin +0 -0
- data/spec/bitcoin/fixtures/rawblock-170.json +68 -0
- data/spec/bitcoin/fixtures/rawblock-9.bin +0 -0
- data/spec/bitcoin/fixtures/rawblock-9.json +39 -0
- data/spec/bitcoin/fixtures/rawblock-testnet-26478.bin +0 -0
- data/spec/bitcoin/fixtures/rawblock-testnet-26478.json +64 -0
- data/spec/bitcoin/fixtures/rawtx-01.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-01.json +27 -0
- data/spec/bitcoin/fixtures/rawtx-02.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-02.json +27 -0
- data/spec/bitcoin/fixtures/rawtx-03.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-03.json +48 -0
- data/spec/bitcoin/fixtures/rawtx-04.json +27 -0
- data/spec/bitcoin/fixtures/rawtx-0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-05.json +23 -0
- data/spec/bitcoin/fixtures/rawtx-14be6fff8c6014f7c9493b4a6e4a741699173f39d74431b6b844fcb41ebb9984.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-2f4a2717ec8c9f077a87dde6cbe0274d5238793a3f3f492b63c744837285e58a.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-2f4a2717ec8c9f077a87dde6cbe0274d5238793a3f3f492b63c744837285e58a.json +27 -0
- data/spec/bitcoin/fixtures/rawtx-406b2b06bcd34d3c8733e6b79f7a394c8a431fbf4ff5ac705c93f4076bb77602.json +23 -0
- data/spec/bitcoin/fixtures/rawtx-52250a162c7d03d2e1fbc5ebd1801a88612463314b55102171c5b5d817d2d7b2.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-b5d4e8883533f99e5903ea2cf001a133a322fa6b1370b18a16c57c946a40823d.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-ba1ff5cd66713133c062a871a8adab92416f1e38d17786b2bf56ac5f6ffdfdf5.json +37 -0
- data/spec/bitcoin/fixtures/rawtx-c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73.json +24 -0
- data/spec/bitcoin/fixtures/rawtx-de35d060663750b3975b7997bde7fb76307cec5b270d12fcd9c4ad98b279c28c.json +23 -0
- data/spec/bitcoin/fixtures/rawtx-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-testnet-a220adf1902c46a39db25a24bc4178b6a88440f977a7e2cabfdd8b5c1dd35cfb.json +27 -0
- data/spec/bitcoin/fixtures/rawtx-testnet-e232e0055dbdca88bbaa79458683195a0b7c17c5b6c524a8d146721d4d4d652f.bin +0 -0
- data/spec/bitcoin/fixtures/rawtx-testnet-e232e0055dbdca88bbaa79458683195a0b7c17c5b6c524a8d146721d4d4d652f.json +41 -0
- data/spec/bitcoin/fixtures/reorg/blk_0_to_4.dat +0 -0
- data/spec/bitcoin/fixtures/reorg/blk_3A.dat +0 -0
- data/spec/bitcoin/fixtures/reorg/blk_4A.dat +0 -0
- data/spec/bitcoin/fixtures/reorg/blk_5A.dat +0 -0
- data/spec/bitcoin/fixtures/testnet/block_0.bin +0 -0
- data/spec/bitcoin/fixtures/testnet/block_1.bin +0 -0
- data/spec/bitcoin/fixtures/testnet/block_2.bin +0 -0
- data/spec/bitcoin/fixtures/testnet/block_3.bin +0 -0
- data/spec/bitcoin/fixtures/testnet/block_4.bin +0 -0
- data/spec/bitcoin/fixtures/testnet/block_5.bin +0 -0
- data/spec/bitcoin/fixtures/txdp-1.txt +32 -0
- data/spec/bitcoin/fixtures/txdp-2-signed.txt +19 -0
- data/spec/bitcoin/fixtures/txdp-2-unsigned.txt +14 -0
- data/spec/bitcoin/key_spec.rb +123 -0
- data/spec/bitcoin/network_spec.rb +48 -0
- data/spec/bitcoin/protocol/addr_spec.rb +68 -0
- data/spec/bitcoin/protocol/alert_spec.rb +20 -0
- data/spec/bitcoin/protocol/block_spec.rb +101 -0
- data/spec/bitcoin/protocol/inv_spec.rb +124 -0
- data/spec/bitcoin/protocol/ping_spec.rb +49 -0
- data/spec/bitcoin/protocol/tx_spec.rb +226 -0
- data/spec/bitcoin/protocol/version_spec.rb +77 -0
- data/spec/bitcoin/reorg_spec.rb +129 -0
- data/spec/bitcoin/script/opcodes_spec.rb +417 -0
- data/spec/bitcoin/script/script_spec.rb +246 -0
- data/spec/bitcoin/spec_helper.rb +36 -0
- data/spec/bitcoin/storage_spec.rb +229 -0
- data/spec/bitcoin/wallet/coinselector_spec.rb +35 -0
- data/spec/bitcoin/wallet/keygenerator_spec.rb +64 -0
- data/spec/bitcoin/wallet/keystore_spec.rb +188 -0
- data/spec/bitcoin/wallet/txdp_spec.rb +74 -0
- data/spec/bitcoin/wallet/wallet_spec.rb +207 -0
- 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
|