bitcoin-ruby 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +4 -1
  2. data/Gemfile +21 -0
  3. data/README.rdoc +85 -25
  4. data/Rakefile +7 -3
  5. data/bin/bitcoin_node +39 -42
  6. data/bin/bitcoin_shell +1 -0
  7. data/bin/bitcoin_wallet +129 -53
  8. data/bitcoin-ruby.gemspec +4 -7
  9. data/concept-examples/blockchain-pow.rb +1 -1
  10. data/doc/CONFIG.rdoc +5 -5
  11. data/doc/EXAMPLES.rdoc +9 -5
  12. data/doc/NAMECOIN.rdoc +34 -0
  13. data/doc/NODE.rdoc +147 -10
  14. data/examples/balance.rb +10 -4
  15. data/examples/bbe_verify_tx.rb +7 -2
  16. data/examples/forwarder.rb +73 -0
  17. data/examples/generate_tx.rb +34 -0
  18. data/examples/simple_network_monitor_and_util.rb +187 -0
  19. data/examples/verify_tx.rb +1 -1
  20. data/lib/bitcoin.rb +308 -18
  21. data/lib/bitcoin/builder.rb +62 -36
  22. data/lib/bitcoin/config.rb +2 -0
  23. data/lib/bitcoin/connection.rb +11 -8
  24. data/lib/bitcoin/electrum/mnemonic.rb +162 -0
  25. data/lib/bitcoin/ffi/openssl.rb +187 -21
  26. data/lib/bitcoin/gui/addr_view.rb +2 -0
  27. data/lib/bitcoin/gui/conn_view.rb +2 -0
  28. data/lib/bitcoin/gui/connection.rb +2 -0
  29. data/lib/bitcoin/gui/em_gtk.rb +2 -0
  30. data/lib/bitcoin/gui/gui.rb +2 -0
  31. data/lib/bitcoin/gui/helpers.rb +2 -0
  32. data/lib/bitcoin/gui/tree_view.rb +2 -0
  33. data/lib/bitcoin/gui/tx_view.rb +2 -0
  34. data/lib/bitcoin/key.rb +77 -11
  35. data/lib/bitcoin/litecoin.rb +81 -0
  36. data/lib/bitcoin/logger.rb +20 -1
  37. data/lib/bitcoin/namecoin.rb +279 -0
  38. data/lib/bitcoin/network/command_client.rb +7 -6
  39. data/lib/bitcoin/network/command_handler.rb +229 -43
  40. data/lib/bitcoin/network/connection_handler.rb +182 -70
  41. data/lib/bitcoin/network/node.rb +231 -106
  42. data/lib/bitcoin/protocol.rb +44 -23
  43. data/lib/bitcoin/protocol/address.rb +5 -3
  44. data/lib/bitcoin/protocol/alert.rb +3 -4
  45. data/lib/bitcoin/protocol/aux_pow.rb +123 -0
  46. data/lib/bitcoin/protocol/block.rb +98 -18
  47. data/lib/bitcoin/protocol/handler.rb +6 -5
  48. data/lib/bitcoin/protocol/parser.rb +44 -19
  49. data/lib/bitcoin/protocol/tx.rb +105 -52
  50. data/lib/bitcoin/protocol/txin.rb +39 -19
  51. data/lib/bitcoin/protocol/txout.rb +28 -13
  52. data/lib/bitcoin/protocol/version.rb +16 -7
  53. data/lib/bitcoin/script.rb +579 -122
  54. data/lib/bitcoin/storage/{dummy.rb → dummy/dummy_store.rb} +8 -14
  55. data/lib/bitcoin/storage/models.rb +20 -7
  56. data/lib/bitcoin/storage/{sequel_store/sequel_migrations.rb → sequel/migrations.rb} +22 -7
  57. data/lib/bitcoin/storage/sequel/migrations/001_base_schema.rb +52 -0
  58. data/lib/bitcoin/storage/sequel/migrations/002_tx.rb +50 -0
  59. data/lib/bitcoin/storage/sequel/migrations/003_change_txin_script_sig_to_blob.rb +18 -0
  60. data/lib/bitcoin/storage/sequel/sequel_store.rb +436 -0
  61. data/lib/bitcoin/storage/storage.rb +233 -28
  62. data/lib/bitcoin/storage/utxo/migrations/001_base_schema.rb +52 -0
  63. data/lib/bitcoin/storage/utxo/migrations/002_utxo.rb +18 -0
  64. data/lib/bitcoin/storage/utxo/utxo_store.rb +361 -0
  65. data/lib/bitcoin/validation.rb +369 -0
  66. data/lib/bitcoin/version.rb +1 -1
  67. data/lib/bitcoin/wallet/coinselector.rb +3 -0
  68. data/lib/bitcoin/wallet/keygenerator.rb +3 -1
  69. data/lib/bitcoin/wallet/keystore.rb +6 -2
  70. data/lib/bitcoin/wallet/txdp.rb +6 -4
  71. data/lib/bitcoin/wallet/wallet.rb +54 -16
  72. data/spec/bitcoin/bitcoin_spec.rb +48 -3
  73. data/spec/bitcoin/builder_spec.rb +40 -17
  74. data/spec/bitcoin/fixtures/000000000000056b1a3d84a1e2b33cde8915a4b61c0cae14fca6d3e1490b4f98.json +3697 -0
  75. data/spec/bitcoin/fixtures/03d7e1fa4d5fefa169431f24f7798552861b255cd55d377066fedcd088fb0e99.json +23 -0
  76. data/spec/bitcoin/fixtures/0961c660358478829505e16a1f028757e54b5bbf9758341a7546573738f31429.json +24 -0
  77. data/spec/bitcoin/fixtures/0f24294a1d23efbb49c1765cf443fba7930702752aba6d765870082fe4f13cae.json +37 -0
  78. data/spec/bitcoin/fixtures/315ac7d4c26d69668129cc352851d9389b4a6868f1509c6c8b66bead11e2619f.json +31 -0
  79. data/spec/bitcoin/fixtures/35e2001b428891fefa0bfb73167c7360669d3cbd7b3aa78e7cad125ddfc51131.json +27 -0
  80. data/spec/bitcoin/fixtures/3a17dace09ffb919ed627a93f1873220f4c975c1248558b18d16bce25d38c4b7.json +72 -0
  81. data/spec/bitcoin/fixtures/3e58b7eed0fdb599019af08578effea25c8666bbe8e200845453cacce6314477.json +27 -0
  82. data/spec/bitcoin/fixtures/514c46f0b61714092f15c8dfcb576c9f79b3f959989b98de3944b19d98832b58.json +24 -0
  83. data/spec/bitcoin/fixtures/51bf528ecf3c161e7c021224197dbe84f9a8564212f6207baa014c01a1668e1e.json +30 -0
  84. data/spec/bitcoin/fixtures/69216b8aaa35b76d6613e5f527f4858640d986e1046238583bdad79b35e938dc.json +28 -0
  85. data/spec/bitcoin/fixtures/7208e5edf525f04e705fb3390194e316205b8f995c8c9fcd8c6093abe04fa27d.json +27 -0
  86. data/spec/bitcoin/fixtures/761d8c5210fdfd505f6dff38f740ae3728eb93d7d0971fb433f685d40a4c04f6.json +27 -0
  87. data/spec/bitcoin/fixtures/aea682d68a3ea5e3583e088dcbd699a5d44d4b083f02ad0aaf2598fe1fa4dfd4.json +27 -0
  88. data/spec/bitcoin/fixtures/bd1715f1abfdc62bea3f605bdb461b3ba1f2cca6ec0d73a18a548b7717ca8531.json +34 -0
  89. data/spec/bitcoin/fixtures/block-testnet-0000000000ac85bb2530a05a4214a387e6be02b22d3348abc5e7a5d9c4ce8dab.bin +0 -0
  90. data/spec/bitcoin/fixtures/cd874fa8cb0e2ec2d385735d5e1fd482c4fe648533efb4c50ee53bda58e15ae2.json +24 -0
  91. data/spec/bitcoin/fixtures/ce5fad9b4ef094d8f4937b0707edaf0a6e6ceeaf67d5edbfd51f660eac8f398b.json +41 -0
  92. data/spec/bitcoin/fixtures/f003f0c1193019db2497a675fd05d9f2edddf9b67c59e677c48d3dbd4ed5f00b.json +23 -0
  93. data/spec/bitcoin/fixtures/freicoin-block-000000005d231b285e63af83edae2d8f5e50e70d396468643092b9239fd3be3c.bin +0 -0
  94. data/spec/bitcoin/fixtures/freicoin-block-000000005d231b285e63af83edae2d8f5e50e70d396468643092b9239fd3be3c.json +43 -0
  95. data/spec/bitcoin/fixtures/freicoin-genesis-block-000000005b1e3d23ecfd2dd4a6e1a35238aa0392c0a8528c40df52376d7efe2c.bin +0 -0
  96. data/spec/bitcoin/fixtures/freicoin-genesis-block-000000005b1e3d23ecfd2dd4a6e1a35238aa0392c0a8528c40df52376d7efe2c.json +67 -0
  97. data/spec/bitcoin/fixtures/litecoin-block-80ca095ed10b02e53d769eb6eaf92cd04e9e0759e5be4a8477b42911ba49c78f.bin +0 -0
  98. data/spec/bitcoin/fixtures/litecoin-block-80ca095ed10b02e53d769eb6eaf92cd04e9e0759e5be4a8477b42911ba49c78f.json +39 -0
  99. data/spec/bitcoin/fixtures/litecoin-genesis-block-12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2.bin +0 -0
  100. data/spec/bitcoin/fixtures/litecoin-genesis-block-12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2.json +39 -0
  101. data/spec/bitcoin/fixtures/rawblock-auxpow.bin +0 -0
  102. data/spec/bitcoin/fixtures/tx-313897799b1e37e9ecae15010e56156dddde4e683c96b0e713af95272c38aee0.json +30 -0
  103. data/spec/bitcoin/fixtures/tx-3da75972766f0ad13319b0b461fd16823a731e44f6e9de4eb3c52d6a6fb6c8ae.json +23 -0
  104. data/spec/bitcoin/fixtures/tx-44b833074e671120ba33106877b49e86ece510824b9af477a3853972bcd8d06a.json +30 -0
  105. data/spec/bitcoin/fixtures/tx-d3d77d63709e47d9ef58f0b557800115a6b676c6a423012fbb96f45d8fcef830.json +28 -0
  106. data/spec/bitcoin/key_spec.rb +128 -3
  107. data/spec/bitcoin/namecoin_spec.rb +182 -0
  108. data/spec/bitcoin/network_spec.rb +5 -3
  109. data/spec/bitcoin/node/command_api_spec.rb +376 -0
  110. data/spec/bitcoin/protocol/addr_spec.rb +2 -0
  111. data/spec/bitcoin/protocol/alert_spec.rb +2 -0
  112. data/spec/bitcoin/protocol/aux_pow_spec.rb +44 -0
  113. data/spec/bitcoin/protocol/block_spec.rb +134 -39
  114. data/spec/bitcoin/protocol/getblocks_spec.rb +32 -0
  115. data/spec/bitcoin/protocol/inv_spec.rb +10 -8
  116. data/spec/bitcoin/protocol/notfound_spec.rb +31 -0
  117. data/spec/bitcoin/protocol/ping_spec.rb +2 -0
  118. data/spec/bitcoin/protocol/tx_spec.rb +83 -17
  119. data/spec/bitcoin/protocol/version_spec.rb +7 -5
  120. data/spec/bitcoin/script/opcodes_spec.rb +412 -133
  121. data/spec/bitcoin/script/script_spec.rb +112 -13
  122. data/spec/bitcoin/spec_helper.rb +68 -0
  123. data/spec/bitcoin/storage/reorg_spec.rb +199 -0
  124. data/spec/bitcoin/storage/storage_spec.rb +337 -0
  125. data/spec/bitcoin/storage/validation_spec.rb +261 -0
  126. data/spec/bitcoin/wallet/coinselector_spec.rb +10 -7
  127. data/spec/bitcoin/wallet/keygenerator_spec.rb +2 -0
  128. data/spec/bitcoin/wallet/keystore_spec.rb +2 -0
  129. data/spec/bitcoin/wallet/txdp_spec.rb +2 -0
  130. data/spec/bitcoin/wallet/wallet_spec.rb +91 -58
  131. metadata +105 -51
  132. data/lib/bitcoin/storage/sequel.rb +0 -335
  133. data/spec/bitcoin/fixtures/0d0affb5964abe804ffe85e53f1dbb9f29e406aa3046e2db04fba240e63c7fdd.json +0 -27
  134. data/spec/bitcoin/fixtures/477fff140b363ec2cc51f3a65c0c58eda38f4d41f04a295bbd62babf25e4c590.json +0 -27
  135. data/spec/bitcoin/reorg_spec.rb +0 -129
  136. data/spec/bitcoin/storage_spec.rb +0 -229
@@ -1,3 +1,7 @@
1
+ # encoding: ascii-8bit
2
+
3
+ require 'json'
4
+
1
5
  # Client to connect to CommandHandler and issue requests or register for events
2
6
  class Bitcoin::Network::CommandClient < EM::Connection
3
7
 
@@ -35,11 +39,7 @@ class Bitcoin::Network::CommandClient < EM::Connection
35
39
  def unbind
36
40
  log.debug { "Disconnected." }
37
41
  callback :disconnected
38
- if @connection_attempts > 1
39
- log.info { "Trying to start server..." }
40
- EM.defer { system("bin/bitcoin_node", "--quiet") }
41
- end
42
- EM.add_timer(1) do
42
+ EM.add_timer(@connection_attempts) do
43
43
  @connection_attempts += 1
44
44
  reconnect(@host, @port)
45
45
  post_init
@@ -86,7 +86,8 @@ class Bitcoin::Network::CommandClient < EM::Connection
86
86
  # register callbacks for monitor
87
87
  def register_monitor_callbacks
88
88
  on_monitor do |type, data|
89
- callback(type, *data)
89
+ type, *params = type.split("_")
90
+ callback(type, *((data || []) + (params || [])))
90
91
  end
91
92
  end
92
93
 
@@ -1,3 +1,5 @@
1
+ # encoding: ascii-8bit
2
+
1
3
  require 'json'
2
4
  require 'monitor'
3
5
 
@@ -29,83 +31,156 @@ class Bitcoin::Network::CommandHandler < EM::Connection
29
31
  # receive request from the client
30
32
  def receive_data data
31
33
  @buf.extract(data).each do |packet|
32
- cmd, args = JSON::parse(packet)
33
- log.debug { [cmd, args] }
34
- if respond_to?("handle_#{cmd}")
35
- respond(cmd, send("handle_#{cmd}", *args))
36
- else
37
- respond(cmd, {:error => "unknown command: #{cmd}. send 'help' for help."})
34
+ begin
35
+ cmd, args = JSON::parse(packet)
36
+ log.debug { [cmd, args] }
37
+ if cmd == "relay_tx"
38
+ handle_relay_tx(*args)
39
+ return
40
+ end
41
+ if respond_to?("handle_#{cmd}")
42
+ respond(cmd, send("handle_#{cmd}", *args))
43
+ else
44
+ respond(cmd, { error: "unknown command: #{cmd}. send 'help' for help." })
45
+ end
46
+ rescue ArgumentError
47
+ respond(cmd, { error: $!.message })
38
48
  end
39
49
  end
40
50
  rescue Exception
41
- p $!
51
+ p $!; puts *$@
42
52
  end
43
53
 
44
54
  # handle +monitor+ command; subscribe client to specified channels
45
- # (+block+, +tx+, +connection+)
55
+ # (+block+, +tx+, +output+, +connection+).
56
+ # Some commands can have parameters, e.g. the number of confirmations
57
+ # +tx+ or +output+ should have. Parameters are appended to the command
58
+ # name after an underscore (_), e.g. subscribe to channel "tx_6" to
59
+ # receive only transactions with 6 confirmations.
60
+ #
61
+ # Receive new blocks:
46
62
  # bitcoin_node monitor block
47
- # bitcoin_node monitor "block tx connection"
63
+ # Receive new (unconfirmed) transactions:
64
+ # bitcoin_node monitor tx
65
+ # Receive transactions with 6 confirmations:
66
+ # bitcoin_node monitor tx_6
67
+ # Receive [txhash, address, value] for each output:
68
+ # bitcoin_node monitor output
69
+ # Receive peer connections/disconnections:
70
+ # bitcoin_node monitor connection"
71
+ # Combine multiple channels:
72
+ # bitcoin_node monitor "block tx tx_1 tx_6 connection"
73
+ #
74
+ # NOTE: When a new block is found, it might include transactions that we
75
+ # didn't previously receive as unconfirmed. To make sure you receive all
76
+ # transactions, also subscribe to the tx_1 channel.
48
77
  def handle_monitor *channels
49
- channels.each do |channel|
50
- @node.notifiers[channel.to_sym].subscribe do |*data|
51
- respond("monitor", [channel, *data])
52
- end
53
- case channel.to_sym
54
- when :block
55
- head = Bitcoin::P::Block.new(@node.store.get_head.to_payload) rescue nil
56
- respond("monitor", ["block", [head, @node.store.get_depth.to_s]]) if head
57
- when :connection
58
- @node.connections.select {|c| c.connected?}.each do |conn|
59
- respond("monitor", [:connection, [:connected, conn.info]])
78
+ channels.map(&:to_sym).each do |channel|
79
+ @node.subscribe(channel) {|*data| respond("monitor", [channel, *data]) }
80
+ name, *params = channel.to_s.split("_")
81
+ send("handle_monitor_#{name}", *params)
82
+ log.info { "Client subscribed to channel #{channel}" }
83
+ end
84
+ nil
85
+ end
86
+
87
+ # Handle +monitor block+ command; send the current chain head
88
+ # after client is subscribed to :block channel
89
+ def handle_monitor_block
90
+ head = Bitcoin::P::Block.new(@node.store.get_head.to_payload) rescue nil
91
+ respond("monitor", ["block", [head, @node.store.get_depth]]) if head
92
+ end
93
+
94
+ # Handle +monitor tx+ command.
95
+ # When +conf+ is given, don't subscribe to the :tx channel for unconfirmed
96
+ # transactions. Instead, subscribe to the :block channel, and whenever a new
97
+ # block comes in, send all transactions that now have +conf+ confirmations.
98
+ def handle_monitor_tx conf = nil
99
+ return unless conf
100
+ if conf.to_i == 0 # 'tx_0' is just an alias for 'tx'
101
+ return @node.subscribe(:tx) {|*a| @node.notifiers[:tx_0].push(*a) }
102
+ end
103
+ @node.subscribe(:block) do |block, depth|
104
+ block = @node.store.get_block_by_depth(depth - conf.to_i + 1)
105
+ next unless block
106
+ block.tx.each {|tx| @node.notifiers["tx_#{conf}".to_sym].push([tx, conf.to_i]) }
107
+ end
108
+ end
109
+
110
+ # Handle +monitor output+ command.
111
+ # Receive tx hash, recipient address and value for each output.
112
+ # This allows easy scanning for new payments without parsing the
113
+ # tx format and running scripts.
114
+ # See #handle_monitor_tx for confirmation behavior.
115
+ def handle_monitor_output conf = 0
116
+ return unless (conf = conf.to_i) > 0
117
+ @node.subscribe(:block) do |block, depth|
118
+ block = @node.store.get_block_by_depth(depth - conf + 1)
119
+ next unless block
120
+ block.tx.each do |tx|
121
+ tx.out.each do |out|
122
+ addr = Bitcoin::Script.new(out.pk_script).get_address
123
+ res = [tx.hash, addr, out.value, conf]
124
+ @node.push_notification("output_#{conf}".to_sym, res)
60
125
  end
61
126
  end
62
127
  end
63
- nil
64
128
  end
65
129
 
66
- # display various statistics
130
+ # Handle +monitor connection+ command; send current connections
131
+ # after client is subscribed to :connection channel.
132
+ def handle_monitor_connection
133
+ @node.connections.select {|c| c.connected?}.each do |conn|
134
+ respond("monitor", [:connection, [:connected, conn.info]])
135
+ end
136
+ end
137
+
138
+ # Get various statistics.
67
139
  # bitcoin_node info
68
140
  def handle_info
69
- blocks = @node.connections.map(&:version).compact.map(&:block) rescue nil
70
- {
71
- :blocks => "#{@node.store.get_depth} (#{(blocks.inject{|a,b| a+=b;a} / blocks.size rescue '?')})#{@node.in_sync ? ' sync' : ''}",
141
+ blocks = @node.connections.map(&:version).compact.map(&:last_block) rescue nil
142
+ established = @node.connections.select {|c| c.state == :connected }
143
+ info = {
144
+ :blocks => "#{@node.store.get_depth} (#{(blocks.inject{|a,b| a+=b; a } / blocks.size rescue '?' )})#{@node.store.in_sync? ? ' sync' : ''}",
72
145
  :addrs => "#{@node.addrs.select{|a| a.alive?}.size} (#{@node.addrs.size})",
73
- :connections => "#{@node.connections.select{|c| c.state == :connected}.size} (#{@node.connections.size})",
146
+ :connections => "#{established.size} established (#{established.select(&:outgoing?).size} out, #{established.select(&:incoming?).size} in), #{@node.connections.size - established.size} connecting",
74
147
  :queue => @node.queue.size,
75
148
  :inv_queue => @node.inv_queue.size,
76
149
  :inv_cache => @node.inv_cache.size,
77
150
  :network => @node.config[:network],
78
151
  :storage => @node.config[:storage],
79
- :version => Bitcoin::Protocol::VERSION,
152
+ :version => Bitcoin.network[:protocol_version],
153
+ :external_ip => @node.external_ip,
80
154
  :uptime => format_uptime(@node.uptime),
81
155
  }
156
+ Bitcoin.namecoin? ? {:names => @node.store.db[:names].count}.merge(info) : info
82
157
  end
83
158
 
84
- # display configuration hash currently used
159
+ # Get the currently active configuration.
85
160
  # bitcoin_node config
86
161
  def handle_config
87
162
  @node.config
88
163
  end
89
164
 
90
- # display connected peers
165
+ # Get currently connected peers.
91
166
  # bitcoin_node connections
92
167
  def handle_connections
93
168
  @node.connections.sort{|x,y| y.uptime <=> x.uptime}.map{|c|
94
- "#{c.host.rjust(15)}:#{c.port} [state: #{c.state}, " +
169
+ "#{c.host.rjust(15)}:#{c.port} [#{c.direction}, state: #{c.state}, " +
95
170
  "version: #{c.version.version rescue '?'}, " +
96
171
  "block: #{c.version.block rescue '?'}, " +
97
172
  "uptime: #{format_uptime(c.uptime) rescue 0}, " +
98
173
  "client: #{c.version.user_agent rescue '?'}]" }
99
174
  end
100
175
 
101
- # connect to given peer(s)
176
+ # Connect to given peer(s).
102
177
  # bitcoin_node connect <ip>:<port>[,<ip>:<port>]
103
178
  def handle_connect *args
104
179
  args.each {|a| @node.connect_peer(*a.split(':')) }
105
180
  {:state => "Connecting..."}
106
181
  end
107
182
 
108
- # disconnect peer(s)
183
+ # Disconnect given peer(s).
109
184
  # bitcoin_node disconnect <ip>:<port>[,<ip>,<port>]
110
185
  def handle_disconnect *args
111
186
  args.each do |c|
@@ -116,21 +191,21 @@ class Bitcoin::Network::CommandHandler < EM::Connection
116
191
  {:state => "Disconnected"}
117
192
  end
118
193
 
119
- # trigger node to ask peers for new blocks
194
+ # Trigger the node to ask its peers for new blocks.
120
195
  # bitcoin_node getblocks
121
196
  def handle_getblocks
122
197
  @node.connections.sample.send_getblocks
123
198
  {:state => "Sending getblocks..."}
124
199
  end
125
200
 
126
- # trigger node to ask for new peer addrs
201
+ # Trigger the node to ask its for new peer addresses.
127
202
  # bitcoin_node getaddr
128
203
  def handle_getaddr
129
204
  @node.connections.sample.send_getaddr
130
205
  {:state => "Sending getaddr..."}
131
206
  end
132
207
 
133
- # display known peer addrs (used by bin/bitcoin_dns_seed)
208
+ # Get known peer addresses (used by bin/bitcoin_dns_seed).
134
209
  # bitcoin_node addrs [count]
135
210
  def handle_addrs count = 32
136
211
  @node.addrs.weighted_sample(count.to_i) do |addr|
@@ -140,28 +215,139 @@ class Bitcoin::Network::CommandHandler < EM::Connection
140
215
  end.compact
141
216
  end
142
217
 
143
- # relay given transaction (in hex)
144
- # bitcoin_node relay_tx <tx data>
145
- def handle_relay_tx data
146
- tx = Bitcoin::Protocol::Tx.from_hash(data)
147
- @node.relay_tx(tx)
218
+ # Trigger a rescan operation when used with a UtxoStore.
219
+ # bitcoin_node rescan
220
+ def handle_rescan
221
+ EM.defer { @node.store.rescan }
222
+ {:state => "Rescanning ..."}
223
+ end
224
+
225
+ # Get Time Since Last Block.
226
+ # bitcoin_node tslb
227
+ def handle_tslb
228
+ { tslb: (Time.now - @node.last_block_time).to_i }
229
+ end
230
+
231
+ # Create a transaction, collecting outputs from given +keys+, spending to +recipients+
232
+ # with an optional +fee+.
233
+ # Keys is an array that can contain either privkeys, pubkeys or addresses.
234
+ # When a privkey is given, the corresponding inputs are signed. If not, the
235
+ # signature_hash is computed and passed along with the response.
236
+ # After creating an unsigned transaction, one just needs to sign the sig_hashes
237
+ # and send everything to #assemble_tx, to receive the complete transaction that
238
+ # can be relayed to the network.
239
+ def handle_create_tx keys, recipients, fee = 0
240
+ keystore = Bitcoin::Wallet::SimpleKeyStore.new(file: StringIO.new("[]"))
241
+ keys.each do |k|
242
+ begin
243
+ key = Bitcoin::Key.from_base58(k)
244
+ key = { addr: key.addr, key: key }
245
+ rescue
246
+ if Bitcoin.valid_address?(k)
247
+ key = { addr: k }
248
+ else
249
+ begin
250
+ key = Bitcoin::Key.new(nil, k)
251
+ key = { addr: key.addr, key: key }
252
+ rescue
253
+ return { error: "Input not valid address, pub- or privkey: #{k}" }
254
+ end
255
+ end
256
+ end
257
+ keystore.add_key(key)
258
+ end
259
+ wallet = Bitcoin::Wallet::Wallet.new(@node.store, keystore)
260
+ tx = wallet.new_tx(recipients.map {|r| [:address, r[0], r[1]]}, fee)
261
+ return { error: "Error creating tx." } unless tx
262
+ [ tx.to_payload.hth, tx.in.map {|i| [i.sig_hash.hth, i.sig_address] rescue nil } ]
148
263
  rescue
149
- {:error => $!}
264
+ { error: "Error creating tx: #{$!.message}" }
150
265
  end
151
266
 
152
- # stop bitcoin node
267
+ # Assemble an unsigned transaction from the +tx_hex+ and +sig_pubkeys+.
268
+ # The +tx_hex+ is the regular transaction structure, with empty input scripts
269
+ # (as returned by #create_tx when called without privkeys).
270
+ # +sig_pubkeys+ is an array of [signature, pubkey] pairs used to build the
271
+ # input scripts.
272
+ def handle_assemble_tx tx_hex, sig_pubs
273
+ tx = Bitcoin::P::Tx.new(tx_hex.htb)
274
+ sig_pubs.each.with_index do |sig_pub, idx|
275
+ sig, pub = *sig_pub.map(&:htb)
276
+ script_sig = Bitcoin::Script.to_signature_pubkey_script(sig, pub)
277
+ tx.in[idx].script_sig_length = script_sig.bytesize
278
+ tx.in[idx].script_sig = script_sig
279
+ end
280
+ tx = Bitcoin::P::Tx.new(tx.to_payload)
281
+ tx.validator(@node.store).validate(raise_errors: true)
282
+ tx.to_payload.hth
283
+ rescue
284
+ { error: "Error assembling tx: #{$!.message}" }
285
+ end
286
+
287
+ # Relay given transaction (in hex).
288
+ # bitcoin_node relay_tx <tx in hex>
289
+ def handle_relay_tx hex, send = 3, wait = 3
290
+ begin
291
+ tx = Bitcoin::P::Tx.new(hex.htb)
292
+ rescue
293
+ return respond("relay_tx", { error: "Error decoding transaction." })
294
+ end
295
+
296
+ validator = tx.validator(@node.store)
297
+ unless validator.validate(rules: [:syntax])
298
+ return respond("relay_tx", { error: "Transaction syntax invalid.",
299
+ details: validator.error })
300
+ end
301
+ unless validator.validate(rules: [:context])
302
+ return respond("relay_tx", { error: "Transaction context invalid.",
303
+ details: validator.error })
304
+ end
305
+
306
+ #@node.store.store_tx(tx)
307
+ @node.relay_tx[tx.hash] = tx
308
+ @node.relay_propagation[tx.hash] = 0
309
+ @node.connections.select(&:connected?).sample(send).each {|c| c.send_inv(:tx, tx.hash) }
310
+
311
+ EM.add_timer(wait) do
312
+ received = @node.relay_propagation[tx.hash]
313
+ total = @node.connections.select(&:connected?).size - send
314
+ percent = 100.0 / total * received
315
+ respond("relay_tx", { success: true, hash: tx.hash, propagation: {
316
+ received: received, sent: 1, percent: percent } })
317
+ end
318
+ rescue
319
+ respond("relay_tx", { error: $!.message, backtrace: $@ })
320
+ end
321
+
322
+ # Stop the bitcoin node.
153
323
  # bitcoin_node stop
154
324
  def handle_stop
155
325
  Thread.start { sleep 0.1; @node.stop }
156
326
  {:state => "Stopping..."}
157
327
  end
158
328
 
159
- # list all commands
329
+ # List all available commands.
160
330
  # bitcoin_node help
161
331
  def handle_help
162
332
  self.methods.grep(/^handle_(.*?)/).map {|m| m.to_s.sub(/^(.*?)_/, '')}
163
333
  end
164
334
 
335
+ # Validate and store given block (in hex) as if it was received by a peer.
336
+ # bitcoin_node store_block <block in hex>
337
+ def handle_store_block hex
338
+ block = Bitcoin::P::Block.new(hex.htb)
339
+ @node.queue << [:block, block]
340
+ { queued: [ :block, block.hash ] }
341
+ end
342
+
343
+ # Store given transaction (in hex) as if it was received by a peer.
344
+ # bitcoin_node store_tx <tx in hex>
345
+ def handle_store_tx hex
346
+ tx = Bitcoin::P::Tx.new(hex.htb)
347
+ @node.queue << [:tx, tx]
348
+ { queued: [ :tx, tx.hash ] }
349
+ end
350
+
165
351
  # format node uptime
166
352
  def format_uptime t
167
353
  mm, ss = t.divmod(60) #=> [4515, 21]
@@ -1,18 +1,24 @@
1
+ # encoding: ascii-8bit
2
+
1
3
  require 'eventmachine'
2
4
 
3
5
  module Bitcoin::Network
4
6
 
5
7
  # Node network connection to a peer. Handles all the communication with a specific peer.
6
- # TODO: incoming/outgoing?
7
8
  class ConnectionHandler < EM::Connection
8
9
 
10
+ LATENCY_MAX = (5*60*1000) # 5min in ms
11
+
9
12
  include Bitcoin
10
13
  include Bitcoin::Storage
11
14
 
12
- attr_reader :host, :port, :state, :version
15
+ attr_reader :host, :port, :version, :direction
13
16
 
14
- def hth(h); h.unpack("H*")[0]; end
15
- def htb(h); [h].pack("H*"); end
17
+ # :new, :handshake, :connected, :disconnected
18
+ attr_reader :state
19
+
20
+ # latency of this connection based on last ping/pong
21
+ attr_reader :latency_ms
16
22
 
17
23
  def log
18
24
  @log ||= Logger::LogWrapper.new("#@host:#@port", @node.log)
@@ -24,12 +30,17 @@ module Bitcoin::Network
24
30
  end
25
31
 
26
32
  # create connection to +host+:+port+ for given +node+
27
- def initialize node, host, port
28
- @node, @host, @port = node, host, port
33
+ def initialize node, host, port, direction
34
+ @node, @host, @port, @direction = node, host, port, direction
29
35
  @parser = Bitcoin::Protocol::Parser.new(self)
30
36
  @state = :new
31
37
  @version = nil
32
38
  @started = nil
39
+ @port, @host = *Socket.unpack_sockaddr_in(get_peername) if get_peername
40
+ @ping_nonce = nil
41
+ @latency_ms = nil
42
+ @lock = Monitor.new
43
+ @last_getblocks = [] # the last few getblocks messages received
33
44
  rescue Exception
34
45
  log.fatal { "Error in #initialize" }
35
46
  p $!; puts $@; exit
@@ -37,36 +48,73 @@ module Bitcoin::Network
37
48
 
38
49
  # check if connection is wanted, begin handshake if it is, disconnect if not
39
50
  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])
51
+ if incoming?
52
+ begin_handshake
42
53
  end
43
- log.info { "Connected to #{@host}:#{@port}" }
44
- @state = :established
45
- @node.connections << self
46
- on_handshake_begin
47
54
  rescue Exception
48
55
  log.fatal { "Error in #post_init" }
49
- p $!; puts $@; exit
56
+ p $!; puts *$@
57
+ end
58
+
59
+ # only called for outgoing connection
60
+ def connection_completed
61
+ begin_handshake
62
+ rescue Exception
63
+ log.fatal { "Error in #connection_completed" }
64
+ p $!; puts *$@
50
65
  end
51
66
 
52
67
  # receive data from peer and invoke Protocol::Parser
53
68
  def receive_data data
54
69
  #log.debug { "Receiving data (#{data.size} bytes)" }
55
- @parser.parse(data)
70
+ @lock.synchronize { @parser.parse(data) }
71
+ rescue
72
+ log.warn { "Error handling data: #{data.hth}" }
73
+ p $!; puts *$@
56
74
  end
57
75
 
58
76
  # connection closed; notify listeners and cleanup connection from node
59
77
  def unbind
60
- log.info { "Disconnected #{@host}:#{@port}" }
61
- @node.notifiers[:connection].push([:disconnected, [@host, @port]])
78
+ log.info { "Disconnected" }
79
+ @node.push_notification(:connection, [:disconnected, [@host, @port]])
62
80
  @state = :disconnected
63
81
  @node.connections.delete(self)
64
82
  end
65
83
 
84
+ # begin handshake
85
+ # TODO: disconnect if we don't complete within a reasonable time
86
+ def begin_handshake
87
+ if incoming? && !@node.accept_connections?
88
+ return close_connection unless @node.config[:connect].include?([@host, @port.to_s])
89
+ end
90
+ log.info { "Established #{@direction} connection" }
91
+ @node.connections << self
92
+ @state = :handshake
93
+ # incoming connections wait to receive a version
94
+ send_version if outgoing?
95
+ rescue Exception
96
+ log.fatal { "Error in #begin_handshake" }
97
+ p $!; puts *$@
98
+ end
99
+
100
+ # complete handshake; set state, started time, notify listeners and add address to Node
101
+ def complete_handshake
102
+ if @state == :handshake
103
+ log.debug { 'Handshake completed' }
104
+ @state = :connected
105
+ @started = Time.now
106
+ @node.push_notification(:connection, [:connected, info])
107
+ @node.addrs << addr
108
+ end
109
+ end
110
+
66
111
  # received +inv_tx+ message for given +hash+.
67
112
  # add to inv_queue, unlesss maximum is reached
68
113
  def on_inv_transaction(hash)
69
- log.debug { ">> inv transaction: #{hth(hash)}" }
114
+ log.debug { ">> inv transaction: #{hash.hth}" }
115
+ if @node.relay_propagation.keys.include?(hash.hth)
116
+ @node.relay_propagation[hash.hth] += 1
117
+ end
70
118
  return if @node.inv_queue.size >= @node.config[:max][:inv]
71
119
  @node.queue_inv([:tx, hash, self])
72
120
  end
@@ -74,7 +122,7 @@ module Bitcoin::Network
74
122
  # received +inv_block+ message for given +hash+.
75
123
  # add to inv_queue, unless maximum is reached
76
124
  def on_inv_block(hash)
77
- log.debug { ">> inv block: #{hth(hash)}" }
125
+ log.debug { ">> inv block: #{hash.hth}" }
78
126
  return if @node.inv_queue.size >= @node.config[:max][:inv]
79
127
  @node.queue_inv([:block, hash, self])
80
128
  end
@@ -82,8 +130,9 @@ module Bitcoin::Network
82
130
  # received +get_tx+ message for given +hash+.
83
131
  # send specified tx if we have it
84
132
  def on_get_transaction(hash)
85
- log.debug { ">> get transaction: #{hash.unpack("H*")[0]}" }
86
- tx = @node.store.get_tx(hash.unpack("H*")[0])
133
+ log.debug { ">> get transaction: #{hash.hth}" }
134
+ tx = @node.store.get_tx(hash.hth)
135
+ tx ||= @node.relay_tx[hash.hth]
87
136
  return unless tx
88
137
  pkt = Bitcoin::Protocol.pkt("tx", tx.to_payload)
89
138
  log.debug { "<< tx: #{tx.hash}" }
@@ -92,16 +141,13 @@ module Bitcoin::Network
92
141
 
93
142
  # received +get_block+ message for given +hash+.
94
143
  # send specified block if we have it
95
- # TODO
96
144
  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)
145
+ log.debug { ">> get block: #{hash.hth}" }
146
+ blk = @node.store.get_block(hash.hth)
147
+ return unless blk
148
+ pkt = Bitcoin::Protocol.pkt("block", blk.to_payload)
149
+ log.debug { "<< block: #{blk.hash}" }
150
+ send_data pkt
105
151
  end
106
152
 
107
153
  # received +addr+ message for given +addr+.
@@ -109,7 +155,7 @@ module Bitcoin::Network
109
155
  def on_addr(addr)
110
156
  log.debug { ">> addr: #{addr.ip}:#{addr.port} alive: #{addr.alive?}, service: #{addr.service}" }
111
157
  @node.addrs << addr
112
- @node.notifiers[:addr].push(addr)
158
+ @node.push_notification(:addr, addr)
113
159
  end
114
160
 
115
161
  # received +tx+ message for given +tx+.
@@ -136,18 +182,23 @@ module Bitcoin::Network
136
182
  # received +version+ message for given +version+.
137
183
  # send +verack+ message and complete handshake
138
184
  def on_version(version)
139
- log.info { ">> version: #{version.version}" }
185
+ log.debug { ">> version: #{version.version}" }
186
+ @node.external_ips << version.to.split(":")[0]
140
187
  @version = version
141
- log.info { "<< verack" }
188
+ log.debug { "<< verack" }
142
189
  send_data( Protocol.verack_pkt )
143
- on_handshake_complete
190
+
191
+ # sometimes other nodes don't bother to send a verack back,
192
+ # but we can consider the handshake complete once we sent ours.
193
+ # apparently it can happen on incoming and outgoing connections alike
194
+ complete_handshake
144
195
  end
145
196
 
146
197
  # received +verack+ message.
147
198
  # complete handshake if it isn't completed already
148
199
  def on_verack
149
- log.info { ">> verack" }
150
- on_handshake_complete if handshake?
200
+ log.debug { ">> verack" }
201
+ complete_handshake if outgoing?
151
202
  end
152
203
 
153
204
  # received +alert+ message for given +alert+.
@@ -156,25 +207,80 @@ module Bitcoin::Network
156
207
  log.warn { ">> alert: #{alert.inspect}" }
157
208
  end
158
209
 
210
+ # received +getblocks+ message.
211
+ # TODO: locator fallback
212
+ def on_getblocks(version, hashes, stop_hash)
213
+ # remember the last few received getblocks messages and ignore duplicate ones
214
+ # fixes unexplained issue where remote node is bombarding us with the same getblocks
215
+ # message over and over (probably related to missing locator fallback handling)
216
+ return if @last_getblocks && @last_getblocks.include?([version, hashes, stop_hash])
217
+ @last_getblocks << [version, hashes, stop_hash]
218
+ @last_getblocks.shift if @last_getblocks.size > 3
219
+
220
+ blk = @node.store.db[:blk][hash: hashes[0].htb.blob]
221
+ depth = blk[:depth] if blk
222
+ log.info { ">> getblocks #{hashes[0]} (#{depth || 'unknown'})" }
223
+
224
+ return unless depth && depth <= @node.store.get_depth
225
+ range = (depth+1..depth+500)
226
+ blocks = @node.store.db[:blk].where(chain: 0, depth: range).select(:hash).all +
227
+ [@node.store.db[:blk].select(:hash)[chain: 0, depth: depth+502]]
228
+ send_inv(:block, *blocks.map {|b| b[:hash].hth })
229
+ end
230
+
231
+ # received +getaddr+ message.
232
+ # send +addr+ message with peer addresses back.
233
+ def on_getaddr
234
+ addrs = @node.addrs.select{|a| a.time > Time.now.to_i - 10800 }.shuffle[0..250]
235
+ log.debug { "<< addr (#{addrs.size})" }
236
+ send_data P::Addr.pkt(*addrs)
237
+ end
238
+
239
+ # begin handshake; send +version+ message
240
+ def send_version
241
+ from = "#{@node.external_ip}:#{@node.config[:listen][1]}"
242
+ version = Bitcoin::Protocol::Version.new({
243
+ :version => 70001,
244
+ :last_block => @node.store.get_depth,
245
+ :from => from,
246
+ :to => @host,
247
+ :user_agent => "/bitcoin-ruby:#{Bitcoin::VERSION}/",
248
+ #:user_agent => "/Satoshi:0.8.3/",
249
+ })
250
+ send_data(version.to_pkt)
251
+ log.debug { "<< version: #{Bitcoin.network[:protocol_version]}" }
252
+ end
253
+
254
+ # send +inv+ message with given +type+ for given +obj+
255
+ def send_inv type, *hashes
256
+ hashes.each_slice(251) do |slice|
257
+ pkt = Protocol.inv_pkt(type, slice.map(&:htb))
258
+ log.debug { "<< inv #{type}: #{slice[0][0..16]}" + (slice.size > 1 ? "..#{slice[-1][0..16]}" : "") }
259
+ send_data(pkt)
260
+ end
261
+ end
262
+
159
263
  # send +getdata tx+ message for given tx +hash+
160
264
  def send_getdata_tx(hash)
161
265
  pkt = Protocol.getdata_pkt(:tx, [hash])
162
- log.debug { "<< getdata tx: #{hth(hash)}" }
266
+ log.debug { "<< getdata tx: #{hash.hth}" }
163
267
  send_data(pkt)
164
268
  end
165
269
 
166
270
  # send +getdata block+ message for given block +hash+
167
271
  def send_getdata_block(hash)
168
272
  pkt = Protocol.getdata_pkt(:block, [hash])
169
- log.debug { "<< getdata block: #{hth(hash)}" }
273
+ log.debug { "<< getdata block: #{hash.hth}" }
170
274
  send_data(pkt)
171
275
  end
172
276
 
173
277
  # send +getblocks+ message
174
278
  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)
279
+ if @node.store.get_depth == -1
280
+ EM.add_timer(3) { send_getblocks }
281
+ return get_genesis_block
282
+ end
283
+ pkt = Protocol.getblocks_pkt(@version.version, locator)
178
284
  log.info { "<< getblocks: #{locator.first}" }
179
285
  send_data(pkt)
180
286
  end
@@ -183,7 +289,7 @@ module Bitcoin::Network
183
289
  def send_getheaders locator = @node.store.get_locator
184
290
  return get_genesis_block if @node.store.get_depth == -1
185
291
  pkt = Protocol.pkt("getheaders", [Bitcoin::network[:magic_head],
186
- locator.size.chr, *locator.map{|l| htb(l).reverse}, "\x00"*32].join)
292
+ locator.size.chr, *locator.map{|l| l.htb_reverse}, "\x00"*32].join)
187
293
  log.debug { "<< getheaders: #{locator.first}" }
188
294
  send_data(pkt)
189
295
  end
@@ -197,30 +303,27 @@ module Bitcoin::Network
197
303
  # send +ping+ message
198
304
  # TODO: wait for pong and disconnect if it doesn't arrive (and version is new enough)
199
305
  def send_ping
200
- nonce = rand(0xffffffff)
201
- log.debug { "<< ping (#{nonce})" }
202
- send_data(Protocol.ping_pkt(nonce))
306
+ if @version.version > Bitcoin::Protocol::BIP0031_VERSION
307
+ @latency_ms = LATENCY_MAX
308
+ @ping_nonce = rand(0xffffffff)
309
+ @ping_time = Time.now
310
+ log.debug { "<< ping (#{@ping_nonce})" }
311
+ send_data(Protocol.ping_pkt(@ping_nonce))
312
+ else
313
+ # set latency to 5 seconds, terrible but this version should be obsolete now
314
+ @latency_ms = (5*1000)
315
+ log.debug { "<< ping" }
316
+ send_data(Protocol.ping_pkt)
317
+ end
203
318
  end
204
319
 
205
320
  # ask for the genesis block
206
321
  def get_genesis_block
207
322
  log.info { "Asking for genesis block" }
208
- pkt = Protocol.getdata_pkt(:block, [htb(Bitcoin::network[:genesis_hash])])
323
+ pkt = Protocol.getdata_pkt(:block, [Bitcoin::network[:genesis_hash].htb])
209
324
  send_data(pkt)
210
325
  end
211
326
 
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
327
  # received +ping+ message with given +nonce+.
225
328
  # send +pong+ message back, if +nonce+ is set.
226
329
  # network versions <=60000 don't set the nonce and don't expect a pong.
@@ -230,28 +333,33 @@ module Bitcoin::Network
230
333
  end
231
334
 
232
335
  # received +pong+ message with given +nonce+.
233
- # TODO: see #send_ping
234
336
  def on_pong nonce
235
- log.debug { ">> pong (#{nonce})" }
337
+ if @ping_nonce == nonce
338
+ @latency_ms = (Time.now - @ping_time) * 1000.0
339
+ end
340
+ log.debug { ">> pong (#{nonce}), latency: #{@latency_ms.to_i}ms" }
236
341
  end
237
342
 
238
343
  # begin handshake; send +version+ message
239
344
  def on_handshake_begin
240
345
  @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)
346
+ from = "#{@node.external_ip}:#{@node.config[:listen][1]}"
347
+ version = Bitcoin::Protocol::Version.new({
348
+ :version => 70001,
349
+ :last_block => @node.store.get_depth,
350
+ :from => from,
351
+ :to => @host,
352
+ :user_agent => "/bitcoin-ruby:#{Bitcoin::VERSION}/",
353
+ #:user_agent => "/Satoshi:0.8.1/",
354
+ })
355
+ send_data(version.to_pkt)
356
+ log.debug { "<< version (#{Bitcoin.network[:protocol_version]})" }
249
357
  end
250
358
 
251
359
  # get Addr object for this connection
252
360
  def addr
253
361
  return @addr if @addr
254
- @addr = Bitcoin::Protocol::Addr.new
362
+ @addr = P::Addr.new
255
363
  @addr.time, @addr.service, @addr.ip, @addr.port =
256
364
  Time.now.tv_sec, @version.services, @host, @port
257
365
  @addr
@@ -265,10 +373,14 @@ module Bitcoin::Network
265
373
  def info
266
374
  {
267
375
  :host => @host, :port => @port, :state => @state,
268
- :version => @version.version, :block => @version.block, :started => @started.to_i,
269
- :user_agent => @version.user_agent
376
+ :version => (@version.version rescue 0), :block => @version.last_block,
377
+ :started => @started.to_i, :user_agent => @version.user_agent
270
378
  }
271
379
  end
380
+
381
+ def incoming?; @direction == :in; end
382
+ def outgoing?; @direction == :out; end
383
+
272
384
  end
273
385
 
274
386
  end