lib/bitcoin/network/command_handler.rb in bitcoin-ruby-0.0.1 vs lib/bitcoin/network/command_handler.rb in bitcoin-ruby-0.0.2
- old
+ new
@@ -1,5 +1,7 @@
+# encoding: ascii-8bit
+
require 'json'
require 'monitor'
# Started by the Node, accepts connections from CommandClient and answers requests or
# registers for events and notifies the clients when they happen.
@@ -27,140 +29,324 @@
end
# receive request from the client
def receive_data data
@buf.extract(data).each do |packet|
- cmd, args = JSON::parse(packet)
- log.debug { [cmd, args] }
- if respond_to?("handle_#{cmd}")
- respond(cmd, send("handle_#{cmd}", *args))
- else
- respond(cmd, {:error => "unknown command: #{cmd}. send 'help' for help."})
+ begin
+ cmd, args = JSON::parse(packet)
+ log.debug { [cmd, args] }
+ if cmd == "relay_tx"
+ handle_relay_tx(*args)
+ return
+ end
+ if respond_to?("handle_#{cmd}")
+ respond(cmd, send("handle_#{cmd}", *args))
+ else
+ respond(cmd, { error: "unknown command: #{cmd}. send 'help' for help." })
+ end
+ rescue ArgumentError
+ respond(cmd, { error: $!.message })
end
end
rescue Exception
- p $!
+ p $!; puts *$@
end
# handle +monitor+ command; subscribe client to specified channels
- # (+block+, +tx+, +connection+)
+ # (+block+, +tx+, +output+, +connection+).
+ # Some commands can have parameters, e.g. the number of confirmations
+ # +tx+ or +output+ should have. Parameters are appended to the command
+ # name after an underscore (_), e.g. subscribe to channel "tx_6" to
+ # receive only transactions with 6 confirmations.
+ #
+ # Receive new blocks:
# bitcoin_node monitor block
- # bitcoin_node monitor "block tx connection"
+ # Receive new (unconfirmed) transactions:
+ # bitcoin_node monitor tx
+ # Receive transactions with 6 confirmations:
+ # bitcoin_node monitor tx_6
+ # Receive [txhash, address, value] for each output:
+ # bitcoin_node monitor output
+ # Receive peer connections/disconnections:
+ # bitcoin_node monitor connection"
+ # Combine multiple channels:
+ # bitcoin_node monitor "block tx tx_1 tx_6 connection"
+ #
+ # NOTE: When a new block is found, it might include transactions that we
+ # didn't previously receive as unconfirmed. To make sure you receive all
+ # transactions, also subscribe to the tx_1 channel.
def handle_monitor *channels
- channels.each do |channel|
- @node.notifiers[channel.to_sym].subscribe do |*data|
- respond("monitor", [channel, *data])
- end
- case channel.to_sym
- when :block
- head = Bitcoin::P::Block.new(@node.store.get_head.to_payload) rescue nil
- respond("monitor", ["block", [head, @node.store.get_depth.to_s]]) if head
- when :connection
- @node.connections.select {|c| c.connected?}.each do |conn|
- respond("monitor", [:connection, [:connected, conn.info]])
+ channels.map(&:to_sym).each do |channel|
+ @node.subscribe(channel) {|*data| respond("monitor", [channel, *data]) }
+ name, *params = channel.to_s.split("_")
+ send("handle_monitor_#{name}", *params)
+ log.info { "Client subscribed to channel #{channel}" }
+ end
+ nil
+ end
+
+ # Handle +monitor block+ command; send the current chain head
+ # after client is subscribed to :block channel
+ def handle_monitor_block
+ head = Bitcoin::P::Block.new(@node.store.get_head.to_payload) rescue nil
+ respond("monitor", ["block", [head, @node.store.get_depth]]) if head
+ end
+
+ # Handle +monitor tx+ command.
+ # When +conf+ is given, don't subscribe to the :tx channel for unconfirmed
+ # transactions. Instead, subscribe to the :block channel, and whenever a new
+ # block comes in, send all transactions that now have +conf+ confirmations.
+ def handle_monitor_tx conf = nil
+ return unless conf
+ if conf.to_i == 0 # 'tx_0' is just an alias for 'tx'
+ return @node.subscribe(:tx) {|*a| @node.notifiers[:tx_0].push(*a) }
+ end
+ @node.subscribe(:block) do |block, depth|
+ block = @node.store.get_block_by_depth(depth - conf.to_i + 1)
+ next unless block
+ block.tx.each {|tx| @node.notifiers["tx_#{conf}".to_sym].push([tx, conf.to_i]) }
+ end
+ end
+
+ # Handle +monitor output+ command.
+ # Receive tx hash, recipient address and value for each output.
+ # This allows easy scanning for new payments without parsing the
+ # tx format and running scripts.
+ # See #handle_monitor_tx for confirmation behavior.
+ def handle_monitor_output conf = 0
+ return unless (conf = conf.to_i) > 0
+ @node.subscribe(:block) do |block, depth|
+ block = @node.store.get_block_by_depth(depth - conf + 1)
+ next unless block
+ block.tx.each do |tx|
+ tx.out.each do |out|
+ addr = Bitcoin::Script.new(out.pk_script).get_address
+ res = [tx.hash, addr, out.value, conf]
+ @node.push_notification("output_#{conf}".to_sym, res)
end
end
end
- nil
end
- # display various statistics
+ # Handle +monitor connection+ command; send current connections
+ # after client is subscribed to :connection channel.
+ def handle_monitor_connection
+ @node.connections.select {|c| c.connected?}.each do |conn|
+ respond("monitor", [:connection, [:connected, conn.info]])
+ end
+ end
+
+ # Get various statistics.
# bitcoin_node info
def handle_info
- blocks = @node.connections.map(&:version).compact.map(&:block) rescue nil
- {
- :blocks => "#{@node.store.get_depth} (#{(blocks.inject{|a,b| a+=b;a} / blocks.size rescue '?')})#{@node.in_sync ? ' sync' : ''}",
+ blocks = @node.connections.map(&:version).compact.map(&:last_block) rescue nil
+ established = @node.connections.select {|c| c.state == :connected }
+ info = {
+ :blocks => "#{@node.store.get_depth} (#{(blocks.inject{|a,b| a+=b; a } / blocks.size rescue '?' )})#{@node.store.in_sync? ? ' sync' : ''}",
:addrs => "#{@node.addrs.select{|a| a.alive?}.size} (#{@node.addrs.size})",
- :connections => "#{@node.connections.select{|c| c.state == :connected}.size} (#{@node.connections.size})",
+ :connections => "#{established.size} established (#{established.select(&:outgoing?).size} out, #{established.select(&:incoming?).size} in), #{@node.connections.size - established.size} connecting",
:queue => @node.queue.size,
:inv_queue => @node.inv_queue.size,
:inv_cache => @node.inv_cache.size,
:network => @node.config[:network],
:storage => @node.config[:storage],
- :version => Bitcoin::Protocol::VERSION,
+ :version => Bitcoin.network[:protocol_version],
+ :external_ip => @node.external_ip,
:uptime => format_uptime(@node.uptime),
}
+ Bitcoin.namecoin? ? {:names => @node.store.db[:names].count}.merge(info) : info
end
- # display configuration hash currently used
+ # Get the currently active configuration.
# bitcoin_node config
def handle_config
@node.config
end
- # display connected peers
+ # Get currently connected peers.
# bitcoin_node connections
def handle_connections
@node.connections.sort{|x,y| y.uptime <=> x.uptime}.map{|c|
- "#{c.host.rjust(15)}:#{c.port} [state: #{c.state}, " +
+ "#{c.host.rjust(15)}:#{c.port} [#{c.direction}, state: #{c.state}, " +
"version: #{c.version.version rescue '?'}, " +
"block: #{c.version.block rescue '?'}, " +
"uptime: #{format_uptime(c.uptime) rescue 0}, " +
"client: #{c.version.user_agent rescue '?'}]" }
end
- # connect to given peer(s)
+ # Connect to given peer(s).
# bitcoin_node connect <ip>:<port>[,<ip>:<port>]
def handle_connect *args
args.each {|a| @node.connect_peer(*a.split(':')) }
{:state => "Connecting..."}
end
- # disconnect peer(s)
+ # Disconnect given peer(s).
# bitcoin_node disconnect <ip>:<port>[,<ip>,<port>]
def handle_disconnect *args
args.each do |c|
host, port = *c.split(":")
conn = @node.connections.select{|c| c.host == host && c.port == port.to_i}.first
conn.close_connection if conn
end
{:state => "Disconnected"}
end
- # trigger node to ask peers for new blocks
+ # Trigger the node to ask its peers for new blocks.
# bitcoin_node getblocks
def handle_getblocks
@node.connections.sample.send_getblocks
{:state => "Sending getblocks..."}
end
- # trigger node to ask for new peer addrs
+ # Trigger the node to ask its for new peer addresses.
# bitcoin_node getaddr
def handle_getaddr
@node.connections.sample.send_getaddr
{:state => "Sending getaddr..."}
end
- # display known peer addrs (used by bin/bitcoin_dns_seed)
+ # Get known peer addresses (used by bin/bitcoin_dns_seed).
# bitcoin_node addrs [count]
def handle_addrs count = 32
@node.addrs.weighted_sample(count.to_i) do |addr|
Time.now.tv_sec + 7200 - addr.time
end.map do |addr|
[addr.ip, addr.port, Time.now.tv_sec - addr.time] rescue nil
end.compact
end
- # relay given transaction (in hex)
- # bitcoin_node relay_tx <tx data>
- def handle_relay_tx data
- tx = Bitcoin::Protocol::Tx.from_hash(data)
- @node.relay_tx(tx)
+ # Trigger a rescan operation when used with a UtxoStore.
+ # bitcoin_node rescan
+ def handle_rescan
+ EM.defer { @node.store.rescan }
+ {:state => "Rescanning ..."}
+ end
+
+ # Get Time Since Last Block.
+ # bitcoin_node tslb
+ def handle_tslb
+ { tslb: (Time.now - @node.last_block_time).to_i }
+ end
+
+ # Create a transaction, collecting outputs from given +keys+, spending to +recipients+
+ # with an optional +fee+.
+ # Keys is an array that can contain either privkeys, pubkeys or addresses.
+ # When a privkey is given, the corresponding inputs are signed. If not, the
+ # signature_hash is computed and passed along with the response.
+ # After creating an unsigned transaction, one just needs to sign the sig_hashes
+ # and send everything to #assemble_tx, to receive the complete transaction that
+ # can be relayed to the network.
+ def handle_create_tx keys, recipients, fee = 0
+ keystore = Bitcoin::Wallet::SimpleKeyStore.new(file: StringIO.new("[]"))
+ keys.each do |k|
+ begin
+ key = Bitcoin::Key.from_base58(k)
+ key = { addr: key.addr, key: key }
+ rescue
+ if Bitcoin.valid_address?(k)
+ key = { addr: k }
+ else
+ begin
+ key = Bitcoin::Key.new(nil, k)
+ key = { addr: key.addr, key: key }
+ rescue
+ return { error: "Input not valid address, pub- or privkey: #{k}" }
+ end
+ end
+ end
+ keystore.add_key(key)
+ end
+ wallet = Bitcoin::Wallet::Wallet.new(@node.store, keystore)
+ tx = wallet.new_tx(recipients.map {|r| [:address, r[0], r[1]]}, fee)
+ return { error: "Error creating tx." } unless tx
+ [ tx.to_payload.hth, tx.in.map {|i| [i.sig_hash.hth, i.sig_address] rescue nil } ]
rescue
- {:error => $!}
+ { error: "Error creating tx: #{$!.message}" }
end
- # stop bitcoin node
+ # Assemble an unsigned transaction from the +tx_hex+ and +sig_pubkeys+.
+ # The +tx_hex+ is the regular transaction structure, with empty input scripts
+ # (as returned by #create_tx when called without privkeys).
+ # +sig_pubkeys+ is an array of [signature, pubkey] pairs used to build the
+ # input scripts.
+ def handle_assemble_tx tx_hex, sig_pubs
+ tx = Bitcoin::P::Tx.new(tx_hex.htb)
+ sig_pubs.each.with_index do |sig_pub, idx|
+ sig, pub = *sig_pub.map(&:htb)
+ script_sig = Bitcoin::Script.to_signature_pubkey_script(sig, pub)
+ tx.in[idx].script_sig_length = script_sig.bytesize
+ tx.in[idx].script_sig = script_sig
+ end
+ tx = Bitcoin::P::Tx.new(tx.to_payload)
+ tx.validator(@node.store).validate(raise_errors: true)
+ tx.to_payload.hth
+ rescue
+ { error: "Error assembling tx: #{$!.message}" }
+ end
+
+ # Relay given transaction (in hex).
+ # bitcoin_node relay_tx <tx in hex>
+ def handle_relay_tx hex, send = 3, wait = 3
+ begin
+ tx = Bitcoin::P::Tx.new(hex.htb)
+ rescue
+ return respond("relay_tx", { error: "Error decoding transaction." })
+ end
+
+ validator = tx.validator(@node.store)
+ unless validator.validate(rules: [:syntax])
+ return respond("relay_tx", { error: "Transaction syntax invalid.",
+ details: validator.error })
+ end
+ unless validator.validate(rules: [:context])
+ return respond("relay_tx", { error: "Transaction context invalid.",
+ details: validator.error })
+ end
+
+ #@node.store.store_tx(tx)
+ @node.relay_tx[tx.hash] = tx
+ @node.relay_propagation[tx.hash] = 0
+ @node.connections.select(&:connected?).sample(send).each {|c| c.send_inv(:tx, tx.hash) }
+
+ EM.add_timer(wait) do
+ received = @node.relay_propagation[tx.hash]
+ total = @node.connections.select(&:connected?).size - send
+ percent = 100.0 / total * received
+ respond("relay_tx", { success: true, hash: tx.hash, propagation: {
+ received: received, sent: 1, percent: percent } })
+ end
+ rescue
+ respond("relay_tx", { error: $!.message, backtrace: $@ })
+ end
+
+ # Stop the bitcoin node.
# bitcoin_node stop
def handle_stop
Thread.start { sleep 0.1; @node.stop }
{:state => "Stopping..."}
end
- # list all commands
+ # List all available commands.
# bitcoin_node help
def handle_help
self.methods.grep(/^handle_(.*?)/).map {|m| m.to_s.sub(/^(.*?)_/, '')}
+ end
+
+ # Validate and store given block (in hex) as if it was received by a peer.
+ # bitcoin_node store_block <block in hex>
+ def handle_store_block hex
+ block = Bitcoin::P::Block.new(hex.htb)
+ @node.queue << [:block, block]
+ { queued: [ :block, block.hash ] }
+ end
+
+ # Store given transaction (in hex) as if it was received by a peer.
+ # bitcoin_node store_tx <tx in hex>
+ def handle_store_tx hex
+ tx = Bitcoin::P::Tx.new(hex.htb)
+ @node.queue << [:tx, tx]
+ { queued: [ :tx, tx.hash ] }
end
# format node uptime
def format_uptime t
mm, ss = t.divmod(60) #=> [4515, 21]