lib/bitcoin/protocol/parser.rb in bitcoin-ruby-0.0.18 vs lib/bitcoin/protocol/parser.rb in bitcoin-ruby-0.0.19

- old
+ new

@@ -1,26 +1,69 @@ # encoding: ascii-8bit module Bitcoin module Protocol - + # https://en.bitcoin.it/wiki/Protocol_documentation#Message_types class Parser attr_reader :stats - def initialize(handler=nil) + def initialize(handler = nil) @h = handler || Handler.new - @buf = "" + @buf = '' @stats = { 'total_packets' => 0, 'total_bytes' => 0, 'total_errors' => 0 } end - def log; @log ||= Bitcoin::Logger.create("parser"); end + # rubocop:disable CyclomaticComplexity + def process_pkt(command, payload) + @stats['total_packets'] += 1 + @stats['total_bytes'] += payload.bytesize + @stats[command] ? (@stats[command] += 1) : @stats[command] = 1 + case command + when 'tx' then @h.on_tx(Tx.new(payload)) + when 'block' then @h.on_block(Block.new(payload)) + when 'headers' then parse_headers(payload) + when 'inv' then parse_inv(payload, :put) + when 'getdata' then parse_inv(payload, :get) + when 'addr' then parse_addr(payload) + when 'getaddr' then @h.on_getaddr if @h.respond_to?(:on_getaddr) + when 'verack' then parse_verack + when 'version' then parse_version(payload) + when 'alert' then parse_alert(payload) + when 'ping' then @h.on_ping(payload.unpack('Q')[0]) + when 'pong' then @h.on_pong(payload.unpack('Q')[0]) + when 'getblocks' then @h.on_getblocks(*parse_getblocks(payload)) \ + if @h.respond_to?(:on_getblocks) + when 'getheaders' then @h.on_getheaders(*parse_getblocks(payload)) \ + if @h.respond_to?(:on_getheaders) + when 'mempool' then handle_mempool_request(payload) + when 'notfound' then handle_notfound_reply(payload) + when 'merkleblock' then parse_mrkle_block(payload) + when 'reject' then handle_reject(payload) + else + parse_error :unknown_packet, [command, payload.hth] + end + end + # rubocop:enable CyclomaticComplexity + def parse_headers(payload) + return unless @h.respond_to?(:on_headers) + buf = StringIO.new(payload) + count = Protocol.unpack_var_int_from_io(buf) + headers = Array.new(count) do + break if buf.eof? + b = Block.new + b.parse_data_from_io(buf, true) + b + end + @h.on_headers(headers) + end + # handles inv/getdata packets - def parse_inv(payload, type=:put) + def parse_inv(payload, type = :put) count, payload = Protocol.unpack_var_int(payload) payload.each_byte.each_slice(36).with_index do |i, idx| - hash = i[4..-1].reverse.pack("C32") + hash = i[4..-1].reverse.pack('C32') case i[0] when 1 type == :put ? @h.on_inv_transaction(hash) : @h.on_get_transaction(hash) when 2 if type == :put @@ -31,164 +74,137 @@ end else @h.on_get_block(hash) end else - parse_error :parse_inv, i.pack("C*") + parse_error :parse_inv, i.pack('C*') end end end def parse_addr(payload) - count, payload = Protocol.unpack_var_int(payload) + _, payload = Protocol.unpack_var_int(payload) payload.each_byte.each_slice(30) do |i| - @h.on_addr(Addr.new(i.pack("C*"))) rescue parse_error(:addr, i.pack("C*")) + begin + @h.on_addr(Addr.new(i.pack('C*'))) + rescue StandardError + parse_error(:addr, i.pack('C*')) + end end end - def parse_headers(payload) - return unless @h.respond_to?(:on_headers) - buf = StringIO.new(payload) - count = Protocol.unpack_var_int_from_io(buf) - headers = count.times.map{ - break if buf.eof? - b = Block.new; b.parse_data_from_io(buf, header_only=true); b - } - @h.on_headers(headers) - end - - def parse_mrkle_block(payload) - return unless @h.respond_to?(:on_mrkle_block) - b = Block.new - b.parse_data_from_io(payload, header_only= :filtered) - @h.on_mrkle_block(b) - end - - def parse_getblocks(payload) - version, payload = payload.unpack('Va*') - count, payload = Protocol.unpack_var_int(payload) - buf, payload = payload.unpack("a#{count*32}a*") - hashes = buf.each_byte.each_slice(32).map{|i| hash = i.reverse.pack("C32").hth } - stop_hash = payload[0..32].reverse_hth - [version, hashes, stop_hash] - end - - def process_pkt(command, payload) - @stats['total_packets'] += 1 - @stats['total_bytes'] += payload.bytesize - @stats[command] ? (@stats[command] += 1) : @stats[command] = 1 - case command - when 'tx'; @h.on_tx( Tx.new(payload) ) - when 'block'; @h.on_block( Block.new(payload) ) - when 'headers'; parse_headers(payload) - when 'inv'; parse_inv(payload, :put) - when 'getdata'; parse_inv(payload, :get) - when 'addr'; parse_addr(payload) - when 'getaddr'; @h.on_getaddr if @h.respond_to?(:on_getaddr) - when 'verack'; @h.respond_to?(:on_verack) ? @h.on_verack : (@h.respond_to?(:on_handshake_complete) ? @h.on_handshake_complete : nil) - when 'version'; parse_version(payload) - when 'alert'; parse_alert(payload) - when 'ping'; @h.on_ping(payload.unpack("Q")[0]) - when 'pong'; @h.on_pong(payload.unpack("Q")[0]) - when 'getblocks'; @h.on_getblocks(*parse_getblocks(payload)) if @h.respond_to?(:on_getblocks) - when 'getheaders'; @h.on_getheaders(*parse_getblocks(payload)) if @h.respond_to?(:on_getheaders) - when 'mempool'; handle_mempool_request(payload) - when 'notfound'; handle_notfound_reply(payload) - when 'merkleblock'; parse_mrkle_block(payload) - when 'reject'; handle_reject(payload) + def parse_verack + if @h.respond_to?(:on_verack) + @h.on_verack else - parse_error :unknown_packet, [command, payload.hth] + @h.respond_to?(:on_handshake_complete) ? @h.on_handshake_complete : nil end end def parse_version(payload) @version = Bitcoin::Protocol::Version.parse(payload) @h.on_version(@version) end def parse_alert(payload) - return unless @h.respond_to?(:on_alert) - @h.on_alert Bitcoin::Protocol::Alert.parse(payload) + # nop (https://github.com/lian/bitcoin-ruby/issues/268) end - def handle_reject(payload) - return unless @h.respond_to?(:on_reject) - @h.on_reject Bitcoin::Protocol::Reject.parse(payload) + def parse_getblocks(payload) + version, payload = payload.unpack('Va*') + count, payload = Protocol.unpack_var_int(payload) + buf, payload = payload.unpack("a#{count * 32}a*") + hashes = buf.each_byte.each_slice(32).map { |i| i.reverse.pack('C32').hth } + stop_hash = payload[0..32].reverse_hth + [version, hashes, stop_hash] end # https://en.bitcoin.it/wiki/BIP_0035 - def handle_mempool_request(payload) - return unless @version.fields[:version] >= 60002 # Protocol version >= 60002 - return unless (@version.fields[:services] & Bitcoin::Protocol::Version::NODE_NETWORK) == 1 # NODE_NETWORK bit set in Services + def handle_mempool_request(*_) + return unless @version.fields[:version] >= 60_002 # Protocol version >= 60002 + return unless ( + @version.fields[:services] & Bitcoin::Protocol::Version::NODE_NETWORK + ) == 1 # NODE_NETWORK bit set in Services @h.on_mempool if @h.respond_to?(:on_mempool) end def handle_notfound_reply(payload) return unless @h.respond_to?(:on_notfound) - count, payload = Protocol.unpack_var_int(payload) + _, payload = Protocol.unpack_var_int(payload) payload.each_byte.each_slice(36) do |i| - hash = i[4..-1].reverse.pack("C32") + hash = i[4..-1].reverse.pack('C32') case i[0] - when 1; @h.on_notfound(:tx, hash) - when 2; @h.on_notfound(:block, hash) + when 1 then @h.on_notfound(:tx, hash) + when 2 then @h.on_notfound(:block, hash) else - parse_error(:notfound, [i.pack("C*"), hash]) + parse_error(:notfound, [i.pack('C*'), hash]) end end end + def parse_mrkle_block(payload) + return unless @h.respond_to?(:on_mrkle_block) + b = Block.new + b.parse_data_from_io(payload, :filtered) + @h.on_mrkle_block(b) + end + + def handle_reject(payload) + return unless @h.respond_to?(:on_reject) + @h.on_reject Bitcoin::Protocol::Reject.parse(payload) + end + def parse(buf) @buf += buf while parse_buffer; end @buf end def parse_buffer - head_magic = Bitcoin::network[:magic_head] + head_magic = Bitcoin.network[:magic_head] head_size = 24 return false if @buf.size < head_size - magic, cmd, length, checksum = @buf.unpack("a4A12Va4") - payload = @buf[head_size...head_size+length] + magic, cmd, length, checksum = @buf.unpack('a4A12Va4') + payload = @buf[head_size...head_size + length] - unless magic == head_magic - handle_stream_error(:close, "head_magic not found") - @buf = '' - else + if magic == head_magic - if Digest::SHA256.digest(Digest::SHA256.digest( payload ))[0...4] != checksum - if (length < 50000) && (payload.size < length) + if Digest::SHA256.digest(Digest::SHA256.digest(payload))[0...4] != checksum + if (length < 50_000) && (payload.size < length) size_info = [payload.size, length].join('/') handle_stream_error(:debug, "chunked packet stream (#{size_info})") else - handle_stream_error(:close, "checksum mismatch") + handle_stream_error(:close, 'checksum mismatch') end return end - @buf = @buf[head_size+length..-1] || "" + @buf = @buf[head_size + length..-1] || '' process_pkt(cmd, payload) + else + handle_stream_error(:close, 'head_magic not found') + @buf = '' end # not empty yet? parse more. - @buf[0] != nil + !@buf[0].nil? end def handle_stream_error(type, msg) + # TODO: replace by writing a real logger/exception handler case type when :close - log.debug {"closing packet stream (#{msg})"} + puts "closing packet stream (#{msg})" else - log.debug { [type, msg] } + puts [type, msg].inspect end end - def parse_error *err + def parse_error(*err) @stats['total_errors'] += 1 return unless @h.respond_to?(:on_error) - @h.on_error *err + @h.on_error(*err) end - - end # Parser - + end end end