peryaudo/bcwallet

View on GitHub
bcwallet.rb

Summary

Maintainability
F
4 days
Test Coverage
#!/usr/bin/env ruby

#
# bcwallet.rb: Educational Bitcoin Client 
#
# This is a tiny Bitcoin client implementation which uses
# Simplified Payment Verification (SPV).
#

# WARNING: This client is for technical education,
# skips a lot of validations, and may have critical bugs.
#
# USE OF THE CLIENT IN MAIN NETWORK MAY CAUSE YOUR COINS LOST.
#
# DO NOT SET THIS VALUE "false".
#
IS_TESTNET = true

# Remote host to use: It is recommended to use this client with a local client.
# Install Bitcoin-Qt and then launch with -testnet option to connect Testnet.
HOST = 'localhost'

# This software is licensed under the MIT license.
#
# The MIT License (MIT)
# 
# Copyright (c) 2014 peryaudo
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

require 'openssl'
require 'socket'

#
# A class which manages both public key and private key in OpenSSL's ECDSA.
#
class Key
  #
  # Bitcoin mainly uses SHA-256(SHA-256(plain)) as a cryptographic hash function
  # when a hash is needed.
  #
  def self.hash256(plain)
    OpenSSL::Digest::SHA256.digest(OpenSSL::Digest::SHA256.digest(plain))
  end

  #
  # RIPEMD-160(SHA-256(plain)) is used when a shorter hash is preferable.
  #
  def self.hash160(plain)
    OpenSSL::Digest::RIPEMD160.digest(OpenSSL::Digest::SHA256.digest(plain))
  end

  BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

  #
  # Modified version of Base58 is used in Bitcoin to convert binaries into human-typable strings.
  #
  def self.encode_base58(plain)
    # plain is big endian

    num = plain.unpack("H*").first.hex

    res = ''

    while num > 0
      res += BASE58[num % 58]
      num /= 58
    end

    # restore leading zeroes
    plain.each_byte do |c|
      break if c != 0
      res += BASE58[0]
    end

    res.reverse
  end

  def self.decode_base58(encoded)
    num = 0
    encoded.each_char do |c|
      num *= 58
      num += BASE58.index(c)
    end

    res = num.to_s(16)

    if res.length % 2 == 1
      res = '0' + res
    end

    # restore leading zeroes
    encoded.each_char do |c|
      break if c != BASE58[0]
      res = '00' + res
    end

    [res].pack('H*')
  end

  #
  # Base58 with the type of data and the checksum is called Base58Check in Bitcoin protocol.
  # It is used as a Bitcoin address, human-readable private key, etc.
  #
  def self.encode_base58check(type, plain)
    leading_bytes = {
      main:    { public_key: 0,   private_key: 128 },
      testnet: { public_key: 111, private_key: 239 }
    }

    leading_byte = [leading_bytes[IS_TESTNET ? :testnet : :main][type]].pack('C')

    data = leading_byte + plain
    checksum = Key.hash256(data)[0, 4]

    Key.encode_base58(data + checksum)
  end

  def self.decode_base58check(encoded)
    decoded = Key.decode_base58(encoded)

    raise "invalid base58 checksum" if Key.hash256(decoded[0, decoded.length - 4])[0, 4] != decoded[-4, 4]

    types = {
      main:    { 0   => :public_key, 128 => :private_key },
      testnet: { 111 => :public_key, 239 => :private_key }
    }

    type = types[IS_TESTNET ? :testnet : :main][decoded[0].unpack('C').first]

    { type: type, data: decoded[1, decoded.length - 5] }
  end

  #
  # Initialize with ASCII-encoded DER format string (nil to generate a new key)
  #
  def initialize(der = nil)
    if der
      @key = OpenSSL::PKey::EC.new([der].pack('H*'))
    else
      @key = OpenSSL::PKey::EC.new('secp256k1')
      @key = @key.generate_key
    end

    @key.check_key
  end

  #
  # Sign the data with the key.
  #
  def sign(data)
    @key.dsa_sign_asn1(data)
  end

  #
  # Convert public key to Bitcoin address.
  #
  def to_address_s
    Key.encode_base58check(:public_key, Key.hash160(@key.public_key.to_bn.to_s(2)))
  end

  # 
  # Convert the private key to Bitcoin private key import format.
  #
  def to_private_key_s
    Key.encode_base58check(:private_key, @key.private_key.to_s(2))
  end

  #
  # Convert the key pair into ASCII-encoded DER format string.
  #
  def to_der_hex_s
    @key.to_der.unpack('H*').first
  end

  def to_public_key
    @key.public_key.to_bn.to_s(2)
  end

  def to_public_key_hash
    Key.hash160(@key.public_key.to_bn.to_s(2))
  end
end

#
# A class which generates Bloom filter.
# Bloom filter is a data structure used in Bitcoin to filter transactions for SPV clients.
# It enables you to quickly test whether an element is included in a set,
# but may have false positives (i.e. probabilistic data structure).
#
class BloomFilter
  public
  #
  # len = length of bloom filter
  # hash_funcs = number of hash functions to use 
  # tweak = a random number
  #
  def initialize(len, hash_funcs, tweak)
    @filter = Array.new(len, 0)
    @hash_funcs = hash_funcs
    @tweak = tweak
  end

  #
  # See an array as a huge little endian integer, and fill idx-th bit
  #
  def set_bit(idx)
    @filter[idx >> 3] |= (1 << (7 & idx))
  end

  def rotate_left_32(x, r)
    ((x << r) | (x >> (32 - r))) & 0xffffffff
  end

  #
  # The hash functions is called MurmurHash3 (32-bit).
  # Reference implementation is somewhat tricky one,
  # so I recommend you to read bitcoinj's one if you want to know the detail.
  #
  def hash(seed, data)
    mask = 0xffffffff

    h1 = seed & mask
    c1 = 0xcc9e2d51
    c2 = 0x1b873593

    data.unpack('V*').each do |k1|
      k1 = (k1 * c1) & mask
      k1 = rotate_left_32(k1, 15)
      k1 = (k1 * c2) & mask

      h1 = h1 ^ k1
      h1 = rotate_left_32(h1, 13)
      h1 = (h1 * 5 + 0xe6546b64) & mask
    end

    padded_remaining_bytes = data[(data.length & (mask ^ 3))..-1] + "\0" * (4 - (data.length & 3))

    k1 = padded_remaining_bytes.unpack('V').first
    k1 = (k1 * c1) & mask
    k1 = rotate_left_32(k1, 15)
    k1 = (k1 * c2) & mask
    h1 = h1 ^ k1

    h1 = (h1 ^ data.length) & mask
    h1 = h1 ^ (h1 >> 16)
    h1 = (h1 * 0x85ebca6b) & mask
    h1 = h1 ^ (h1 >> 13)
    h1 = (h1 * 0xc2b2ae35) & mask
    h1 = h1 ^ (h1 >> 16)

    h1
  end

  #
  # Insert the data into the Bloom filter
  #
  def insert(data)
    @hash_funcs.times do |i|
      set_bit(hash(i * 0xfba4c795 + @tweak, data) % (@filter.length * 8))
    end
  end

  def to_s
    res = ''
    @filter.each do |byte|
      res += [byte].pack('C')
    end
    res
  end
end

#
# A class for message serializers and deserializers.
# It contains type definitions of structures.
#
class Message
  #
  # Constants used in inventory vector
  #
  MSG_TX = 1
  MSG_BLOCK = 2
  MSG_FILTERED_BLOCK = 3


  def initialize
    #
    # Message definitions.
    #
    @message_definitions = {
      version: [
        [:version,   :uint32],
        [:services,  :uint64],
        [:timestamp, :uint64],
        [:your_addr, :net_addr],
        [:my_addr,   :net_addr],
        [:nonce,     :uint64],
        [:agent,     :string],
        [:height,    :uint32],
        [:relay,     :relay_flag]
      ],
      ping:    [[:nonce, :uint64]],
      pong:    [[:nonce, :uint64]],
      alert:   [],
      verack:  [],
      mempool: [],
      addr:    [[:addr, array_for(:net_addr)]],
      inv:     [[:inventory,  array_for(:inv_vect)]],
      merkleblock: [
        [:hash,        :block_hash],
        [:version,     :uint32],
        [:prev_block,  :hash256],
        [:merkle_root, :hash256],
        [:timestamp,   :uint32],
        [:bits,        :uint32],
        [:nonce,       :uint32],
        [:total_txs,   :uint32],
        [:hashes,      array_for(:hash256)],
        [:flags,       :string]
      ],
      tx: [
        [:hash,      :tx_hash],
        [:version,   :uint32],
        [:tx_in,     array_for(:tx_in)],
        [:tx_out,    array_for(:tx_out)],
        [:lock_time, :uint32]
      ],
      filterload: [
        [:filter,     :string],
        [:hash_funcs, :uint32],
        [:tweak,      :uint32],
        [:flag,       :uint8]
      ],
      getblocks: [
        [:version,       :uint32],
        [:block_locator, array_for(:hash256)],
        [:hash_stop,     :hash256]
      ],
      getdata: [[:inventory, array_for(:inv_vect)]],
      inv_vect: [
        [:type, :uint32],
        [:hash, :hash256]],
      outpoint: [
        [:hash, :hash256],
        [:index, :uint32]],
      tx_in: [
        [:previous_output, :outpoint],
        [:signature_script, :string],
        [:sequence, :uint32]],
      tx_out: [
        [:value, :uint64],
        [:pk_script, :string]]
    }
  end

  #
  # Serialize a message using message definitions.
  #
  def serialize(message)
    @payload = ''

    serialize_struct(message[:command], message)

    @payload
  end

  #
  # Deserialize a message using message definitions.
  #
  def deserialize(command, payload)
    raise unless @message_definitions.has_key?(command)

    @payload = payload

    res = deserialize_struct(command)
    res[:command] = command

    res
  end

  # 
  # Read a message and parse it using message definitions.
  #
  def read(socket)
    packet = read_packet(socket)

    expected_magic    = [IS_TESTNET ? '0b110907' : 'f9beb4d9'].pack('H*')
    expected_checksum = Key.hash256(packet[:payload])[0, 4]

    if packet[:magic] != expected_magic
      raise 'invalid magic received'
    end

    if packet[:checksum] != expected_checksum
      raise 'incorrect checksum'
    end

    unless @message_definitions.has_key?(packet[:command])
      raise 'invalid message type'
    end

    deserialize(packet[:command], packet[:payload])
  end

  #
  # Actually send a message to the remote host.
  #
  def write(socket, message)
    # Create payload
    payload = serialize(message)

    # 4bytes: magic
    raw_message = [IS_TESTNET ? '0b110907' : 'f9beb4d9'].pack('H*')

    # 12bytes: command (padded with zeroes)
    raw_message += [message[:command].to_s].pack('a12')

    # 4bytes: length of payload
    raw_message += [payload.length].pack('V')

    # 4bytes: checksum
    raw_message += Key.hash256(payload)[0, 4]

    # payload
    raw_message += payload

    socket.write raw_message
    socket.flush
  end

  private

  def read_packet(socket)
    magic    = socket.read(4)
    command  = socket.read(12).unpack('A12').first.to_sym
    length   = socket.read(4).unpack('V').first
    checksum = socket.read(4)
    payload  = socket.read(length)

    { magic: magic, command: command, checksum: checksum, payload: payload }
  end

  def serialize_struct(type, struct)
    if type.kind_of?(Proc)
      type.call(:write, struct)
      return
    end

    if @message_definitions.has_key?(type)
      @message_definitions[type].each do |definition|
        next if struct.has_key?(:command) && definition.first == :hash
        serialize_struct(definition.last, struct[definition.first])
      end
    else
      method(type).call(:write, struct)
    end
  end

  def deserialize_struct(type)
    if type.kind_of?(Proc)
      return type.call(:read)
    end

    if @message_definitions.has_key?(type)
      res = {}
      @message_definitions[type].each do |definition|
        res[definition.first] = deserialize_struct(definition.last)
      end
      res
    else
      method(type).call(:read)
    end
  end

  #
  # Higher order function to generate array serializer / deserializer
  #
  def array_for(elm)
    lambda do |rw, val = nil|
      case rw
      when :read
        count = integer(:read)
        res = []
        count.times do
          res.push deserialize_struct(elm)
        end
        res
      when :write
        integer(:write, val.length)
        val.each do |v|
          serialize_struct(elm, v)
        end
        val
      end
    end
  end

  #
  # Serializer & deserializer methods
  #

  def read_bytes(len)
    res = @payload[0, len]
    @payload = @payload[len..-1]
    res
  end

  def write_bytes(val)
    @payload += val
  end

  def fixed_integer(templ, len, rw, val = nil)
    case rw
    when :read 
      read_bytes(len).unpack(templ).first
    when :write
      write_bytes([val].pack(templ))
    end
  end

  def uint8(rw, val = nil)
    fixed_integer('C', 1, rw, val)
  end

  def uint16(rw, val = nil)
    fixed_integer('v', 2, rw, val)
  end

  def uint32(rw, val = nil)
    fixed_integer('V', 4, rw, val)
  end

  def uint64(rw, val = nil)
    fixed_integer('Q', 8, rw, val)
  end

  def read_integer
    top = uint8(:read)
    case top
    when 0xfd then uint16(:read)
    when 0xfe then uint32(:read)
    when 0xff then uint64(:read)
    else top
    end
  end

  def write_integer(val)
    if val < 0xfd
      uint8(:write, val)
    elsif val <= 0xffff
      uint8(:write, 0xfd)
      uint16(:write, val)
    elsif val <= 0xffffffff
      uint8(:write, 0xfe)
      uint32(:write, val)
    else
      uint8(:write, 0xff)
      uint64(:write, val)
    end
  end

  def integer(rw, val = nil)
    case rw
    when :read
      read_integer
    when :write
      write_integer(val)
    end
  end

  def string(rw, val = nil)
    case rw
    when :read
      len = integer(:read)
      read_bytes(len)
    when :write
      integer(:write, val.length)
      write_bytes(val)
      val
    end
  end

  def net_addr(rw, val = nil)
    # accurate serializing is not necessary
    case rw
    when :read
      read_bytes(26)
      nil
    when :write
      write_bytes([0, '00000000000000000000FFFF', '00000000', 8333].pack('QH*H*n'))
      val
    end
  end

  def relay_flag(rw, val = nil)
    case rw
    when :read
      if @payload.length > 0
        uint8(:read)
      else
        true
      end
    when :write
      unless val
        uint8(:write, 0)
      end
      val
    end
  end

  def hash256(rw, val = nil)
    case rw
    when :read
      read_bytes(32)
    when :write
      write_bytes(val)
      val
    end
  end

  def block_hash(rw, val = nil)
    case rw
    when :read
      Key.hash256(@payload[0, 80])
    end
  end

  def tx_hash(rw, val = nil)
    case rw
    when :read
      Key.hash256(@payload)
    end
  end
end

#
# The blockchain class. It manages and stores Bitcoin blockchain data.
#
class Blockchain
  def initialize(keys, data_file_name)
    @data_file_name = data_file_name

    keys_hash = Key.hash256(keys.collect { |key, _| key }.sort.join)

    init_data(keys_hash)
    load_data

    # new keys are added since the last synchronization
    init_data(keys_hash) if @data[:keys_hash] != keys_hash
  end

  def init_data(keys_hash)
    @data = { blocks: {}, txs: {}, last_height: 0, keys_hash: keys_hash }
  end

  def load_data
    return unless File.exists?(@data_file_name)

    open(@data_file_name, 'rb') do |file|
      @data = Marshal.restore(file)
    end
  end

  def save_data
    open(@data_file_name, 'wb') do |file|
      Marshal.dump @data, file
    end
  end

  def calc_last_hash
    # These hashes are genesis blocks' ones.
    last_hash = { timestamp: 0,
                   hash: [IS_TESTNET ?
                     '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943' :
                     '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'].pack('H*').reverse }

    @data[:blocks].each do |hash, block|
      if block[:timestamp] > last_hash[:timestamp]
        last_hash = { timestamp: block[:timestamp], hash: hash }
      end
    end

    last_hash
  end

  def blocks
    @data[:blocks]
  end

  def txs
    @data[:txs]
  end

  def last_height
    @data[:last_height]
  end

  def last_height=(v)
    @data[:last_height] = v
  end

  #
  # Get balance for the keys
  #
  def get_balance(keys)
    balance = {}
    keys.each do |addr, _|
      balance[addr] = 0
    end

    set_spent_for_tx_outs!

    @data[:txs].each do |tx_hash, tx|
      keys.each do |addr, key|
        public_key_hash = key.to_public_key_hash

        tx[:tx_out].each do |tx_out|
          # The tx_out was already spent
          next if tx_out[:spent]

          if extract_public_key_hash_from_script(tx_out[:pk_script]) == public_key_hash
            balance[addr] += tx_out[:value]
          end
        end
      end
    end

    balance
  end

  #
  # This is a heuristic function to find out whether the block is an independent young block.
  # An independent block here means a block which have not received one of its ancestors yet.
  # We may receive this kind of blocks regardless of getblocks -> inv -> getdata iteration.
  #
  # To implement it more robustly, you have to construct graph from received blocks,
  # do a lot of validations, and actually take the longest block chain.
  #
  # The reason why the client took this way is simplicity and performance.
  # Doing them in Ruby is painful, and also it's not ciritical to explain how Bitcoin client works.
  #
  def is_young_block(hash)
    (@data[:blocks][hash][:timestamp] - Time.now.to_i).abs <= 60 * 60 && !is_too_high(hash)
  end

  def accumulate_txs(from_key, amount)
    public_key_hash = from_key.to_public_key_hash

    # Refresh spent flags of tx_outs
    set_spent_for_tx_outs!

    # In a real SPV client, we should walk along merkle trees to validate the transaction.
    # It will be implemented to this client soon.
    total_satoshis = 0
    tx_in = []
    @data[:txs].each do |tx_hash, tx|
      break if total_satoshis >= amount

      matched = nil
      pk_script = nil

      tx[:tx_out].each_with_index do |tx_out, index|
        next if tx_out[:spent]

        if extract_public_key_hash_from_script(tx_out[:pk_script]) == public_key_hash
          total_satoshis += tx_out[:value]
          matched = index
          pk_script = tx_out[:pk_script]
          break
        end
      end

      if matched
        tx_in.push({ previous_output:  { :hash => tx[:hash], :index => matched },
                     signature_script: '',
                     sequence:         ((1 << 32) - 1),

                     # not included in serialized data, but used to make signature
                     pk_script: pk_script })
      end
    end

    { total_satoshis: total_satoshis, tx_in: tx_in }
  end

  #
  # Set spent flags for all tx_outs.
  # If the tx_out is already spent on another transaction's tx_in, it will be set.
  # 
  def set_spent_for_tx_outs!
    @data[:txs].each do |tx_hash, tx|
      tx[:tx_in].each do |tx_in|
        hash = tx_in[:previous_output][:hash]
        index = tx_in[:previous_output][:index]
        if @data[:txs].has_key?(hash)
          @data[:txs][hash][:tx_out][index][:spent] = true
        end
      end
    end
  end

  private

  #
  # This checks whether the block has previous 5 (= threshold) blocks
  # in the received data.
  #
  def is_too_high(hash)
    threshold = 5
    cur = 0
    while @data[:blocks].has_key?(hash) && cur < threshold
      hash = @data[:blocks][hash][:prev_block]
      cur += 1
    end
    cur == threshold
  end

  #
  # Bitcoin has complex scripting system for its payment,
  # but we will only support very basic one.
  #
  def extract_public_key_hash_from_script(script)
    # OP_DUP OP_HASH160 (public key hash) OP_EQUALVERIFY OP_CHECKSIG
    unless script[0, 3]  == ['76a914'].pack('H*') &&
           script[23, 2] == ['88ac'].pack('H*') &&
           script.length == 25
      raise 'unsupported script format' 
    end

    script[3, 20]
  end
end

#
# The network class. It may be split into two or three classes
# to manage multiple connections and features in production.
#
class Network
  attr_reader :status, :data

  # 
  # keys = { name => ECDSA key objects }
  #
  def initialize(keys, data_file_name)
    @message = Message.new

    @keys = keys

    @blockchain = Blockchain.new(@keys, data_file_name)
    @last_hash = @blockchain.calc_last_hash

    @is_sync_finished = true

    @requested_data = 0
    @received_data = 0
  end

  #
  # Synchronize the block chain.
  # It creates a new thread and returns immediately.
  # To know whether the thread was finished, use Network#sync_finished?
  #
  def sync
    Thread.abort_on_exception = true
    @is_sync_finished = false
    t = Thread.new do

      unless @socket
        @status = 'connection establishing ... '

        @socket = TCPSocket.open(HOST, IS_TESTNET ? 18333 : 8333)

        send_version
      end

      if @created_transaction
        @status = 'announcing transaction ... '

        send_transaction_inv
      end

      loop do
        break if dispatch_message
      end

      @is_sync_finished = true
    end
    t.run
  end

  def sync_finished?
    @is_sync_finished
  end

  # 
  # Send coins to the address.
  # from_key = Key object which the client sends coins from
  # to_addr  = Receiving address (string)
  # transaction_fee = Transaction fee which miners receive
  #
  def send(from_key, to_addr, amount, transaction_fee = 0)
    # The process of announcing a created transaction is as follows: 
    #   Generate tx message and get its hash, and send inv message with the hash to the remote host.
    #   Then the remote host will send getdata, so you can now actually send tx message.

    to_addr_decoded = Key.decode_base58check(to_addr)
    if to_addr_decoded[:type] != :public_key
      raise 'invalid address'
    end

    accumulated = @blockchain.accumulate_txs(from_key, amount)

    payback = accumulated[:total_satoshis] - amount - transaction_fee
    unless payback >= 0
      raise "you don't have enough balance to pay"
    end

    @created_transaction = sign_transaction(from_key, {
      command: :tx,

      version: 1,

      tx_in: accumulated[:tx_in],
      tx_out: [{ value: amount,  pk_script: generate_pk_script(to_addr_decoded[:data]) },
               { value: payback, pk_script: generate_pk_script(from_key.to_public_key_hash) }],

      lock_time: 0
    })

    @status = ''
  end

  #
  # Get balance for the keys
  #
  def get_balance
    @blockchain.get_balance(@keys)
  end


  def block(hash)
    @blockchain.blocks[hash]
  end

  private

  PROTOCOL_VERSION = 70002

  #
  # Send version message to the remote host.
  #
  def send_version
    @message.write(@socket, {
      command: :version,

      version:   PROTOCOL_VERSION,

      # This client should not be asked for full blocks.
      services:  0,

      timestamp: Time.now.to_i,

      your_addr: nil, # I found that at least Satoshi client doesn't check it,
      my_addr:   nil, # so it will be enough for this client.
      
      nonce:     (rand(1 << 64) - 1), # A random number.

      agent:     '/bcwallet.rb:1.00/',
      height:    (@blockchain.blocks.length - 1), # Height of possessed blocks

      # It forces the remote host not to send any 'inv' messages till it receive 'filterload' message.
      relay:     false
    })
  end

  #
  # Send filterload message.
  #
  def send_filterload
    hash_funcs = 10
    tweak = rand(1 << 32) - 1

    bf = BloomFilter.new(512, hash_funcs, tweak) 

    @keys.each do |_, key|
      bf.insert(key.to_public_key)
      bf.insert(key.to_public_key_hash)
    end

    @message.write(@socket, {
      command: :filterload,

      filter:     bf.to_s,
      hash_funcs: hash_funcs,
      tweak:      tweak,

      # BLOOM_UPDATE_ALL, updates Bloom filter automatically when the client has found matching transactions.
      flag:       1
    })
  end

  def refresh_status
    weight = 50
    perc = (weight * @blockchain.blocks.length / @blockchain.last_height).to_i
    @status = '|' + '=' * perc + '_' * [weight - perc, 0].max +
      "| #{(@blockchain.blocks.length - 1)} / #{@blockchain.last_height} "
  end

  #
  # Send getblocks message until it receive all the blocks.
  # If it receives all the blocks, it will return true. Otherwise, it returns false.
  #
  def send_getblocks
    refresh_status

    # @blockchain.blocks.length includes block #0 while @blockchain.last_height does not.
    if @blockchain.blocks.length > @blockchain.last_height
      @blockchain.save_data
      return true
    end

    if @blockchain.blocks.empty?
      send_getdata([{type: Message::MSG_FILTERED_BLOCK, hash: @last_hash[:hash]}])
    end

    @message.write(@socket, {
      command: :getblocks,

      version: PROTOCOL_VERSION,
      block_locator: [@last_hash[:hash]],
      hash_stop: ['00' * 32].pack('H*')
    })

    false
  end

  #
  # Send getdata message for the inventory while rewriting MSG_BLOCK to MSG_FILTERED_BLOCK
  #
  def send_getdata(inventory)
    @message.write(@socket, {
      command: :getdata,

      inventory: inventory.collect do |elm|
        # receive merkleblock instead of usual block
        {type: (elm[:type] == Message::MSG_BLOCK ? Message::MSG_FILTERED_BLOCK : elm[:type]),
         hash: elm[:hash]}
      end
    })
  end

  #
  # Send inv message when you created a transaction
  #
  def send_transaction_inv
    payload = @message.serialize(@created_transaction)

    @created_transaction[:hash] = Key.hash256(payload)
    
    @message.write(@socket, {
      command: :inv,
      inventory: [{type: Message::MSG_TX, hash: @created_transaction[:hash]}]
    })
  end

  #
  # Send transaction message you created
  #
  def send_transaction
    @message.write(@socket, @created_transaction)

    @socket.flush

    sleep 30

    @blockchain.txs[@created_transaction[:hash]] = @created_transaction

    @blockchain.save_data
  end

  #
  # Dispatch messages. It reads message from the remote host,
  # send proper messages back, and then again wait for a message.
  #
  # Returns true if the whole process has been finished, otherwise false.
  #
  def dispatch_message
    message = @message.read(@socket)

    case message[:command]
    when :version     then dispatch_version(message)
    when :ping        then dispatch_ping(message)
    when :inv         then dispatch_inv(message)
    when :merkleblock then dispatch_merkleblock(message)
    when :tx          then dispatch_tx(message)
    when :getdata     then dispatch_getdata(message)
    end
  end

  def dispatch_version(message)
    # This is handshake process: 

    # Local -- version -> Remote
    # Local <- version -- Remote
    # Local -- verack  -> Remote
    # Local <- verack  -- Remote
    # Local <- ping    -- Remote
    # Local -- pong    -> Remote

    # You've got the latest block height.
    @blockchain.last_height = message[:height]
    @blockchain.save_data

    @message.write(@socket, {command: :verack})

    false
  end

  def dispatch_ping(message)
    # Reply with pong
    @message.write(@socket, {command: :pong, nonce: message[:nonce]})

    # Handshake finished, so you can do anything you want.

    # Set Bloom filter
    send_filterload

    # Tell the remote host to send transactions (inv) it has in its memory pool.
    @message.write(@socket, {command: :mempool})

    # Send getblocks on demand and return true
    send_getblocks

  end

  def dispatch_inv(message)
    send_getdata message[:inventory]

    # Memorize number of requests to check whether the client have received all transactions it required.
    @requested_data += message[:inventory].length

    false
  end

  def dispatch_merkleblock(message)
    @received_data += 1

    @blockchain.blocks[message[:hash]] = message

    # Described in Blockchain#is_young_block.
    # It supposes that blocks are sent in its height order. Don't try this in production code.
    unless @blockchain.is_young_block(message[:hash])
      @last_hash = { timestamp: message[:timestamp], hash: message[:hash] }
    end

    @requested_data <= @received_data && send_getblocks
  end

  def dispatch_tx(message)
    @received_data += 1

    @blockchain.txs[message[:hash]] = message

    @requested_data <= @received_data && send_getblocks
  end

  def dispatch_getdata(message)
    @status = 'sending transaction data ... '

    # Send the transaction you create
    send_transaction

    true
  end

  def sign_transaction(from_key, transaction)
    # We have generated all data without signatures, so we're now going to generate signatures.
    # However, it is very complicated one.
    signatures = []

    transaction[:tx_in].length.times do |i|
      signatures.push(sign_transaction_of_idx(i))
    end

    signatures.each_with_index do |signature, i|
      transaction[:tx_in][i][:signature_script] = generate_signature_script(signature, from_key)
    end

    return transaction
  end

  def sign_transaction_of_idx(from_key, transaction, i)
    duplicated            = transaction.dup
    duplicated[:tx_in]    = duplicated[:tx_in].dup
    duplicated[:tx_in][i] = duplicated[:tx_in][i].dup

    # To generate signature, you need hash256 of the whole transaction in special form.
    # The transaction in that form is different in a way that the signature_script
    # field in the tx_in to sign is replaced with pk_script in previous tx_out,
    # and other tx_ins' signature_scripts are empty.
    # (make sure that var_int for the length is also set to zero)
    #
    # For further information, see: 
    #   https://en.bitcoin.it/w/images/en/7/70/Bitcoin_OpCheckSig_InDetail.png
    #

    duplicated[:tx_in][i][:signature_script] = transaction[:tx_in][i][:pk_script]

    payload = @message.serialize(duplicated)

    # hash256 includes type code field (see the figure in the URL above)
    verified_str = Key.hash256(payload + [1].pack('V'))

    from_key.sign(verified_str)
  end

  def generate_pk_script(public_key_hash)
    # pk_script field is constructed in Bitcoin's scripting system
    #    https://en.bitcoin.it/wiki/Script
    #
    prefix = ['76a914'].pack('H*') # OP_DUP OP_HASH160 [length of the address]
    postfix = ['88ac'].pack('H*')  # OP_EQUALVERIFY OP_CHECKSIG

    prefix + public_key_hash + postfix
  end

  def generate_signature_script(signature, from_key)
    # see the figure in the
    #   https://en.bitcoin.it/w/images/en/7/70/Bitcoin_OpCheckSig_InDetail.png

    public_key = from_key.to_public_key

    script = [signature.length + 1].pack('C') + signature + [1].pack('C')
    script += [public_key.length].pack('C') + public_key

    script
  end
end


#
# The class deals with command line arguments and the key file.
#
class BCWallet
  def initialize(argv, keys_file_name, data_file_name)
    @argv = argv
    @keys_file_name = keys_file_name
    @data_file_name = data_file_name
    @network = nil
  end

  def run
    return usage if @argv.length < 1

    # check argument numbers
    case @argv.first
    when 'generate' then return if require_args(1)
    when 'list'     then return if require_args(0)
    when 'export'   then return if require_args(1)
    when 'balance'  then return if require_args(0)
    when 'send'     then return if require_args(3)
    when 'block'    then return if require_args(1)
    else
      usage 'invalid command'
      return
    end

    load_keys

    case @argv.first
    when 'generate' then generate(@argv[1]) # name
    when 'list'     then list
    when 'export'   then export(@argv[1]) # name
    when 'balance'  then balance
    when 'send'     then send(@argv[1], @argv[2], btc_to_satoshi(@argv[3].to_r)) # name, to, amount
    when 'block'    then block(@argv[1]) # hash
    end
  end

  private

  def usage(error = nil)
    warn "bcwallet.rb: #{error}\n\n" if error
    warn "bcwallet.rb: Educational Bitcoin Client"
    warn "Usage: ruby bcwallet.rb <command> [<args>]"
    warn "commands:"
    warn "    generate <name>\t\tgenerate a new Bitcoin address"
    warn "    list\t\t\tshow list for all Bitcoin addresses"
    warn "    export <name>\t\tshow private key for the Bitcoin address"
    warn "    balance\t\t\tshow balances for all Bitcoin addresses"
    warn "    send <name> <to> <amount>\ttransfer coins to the Bitcoin address"
  end

  def require_args(number)
    if @argv.length < number + 1
      usage 'missing arguments'
      true
    else
      false
    end
  end

  def load_keys
    @keys = {}

    return unless File.exists?(@keys_file_name)

    open(@keys_file_name, 'r') do |file|
      file.read.lines.each do |line|
        name, der = line.split(' ')
        @keys[name] = Key.new(der)
      end
    end
  end

  def init_network
    @network = Network.new(@keys, @data_file_name)
  end

  def wait_for_sync(mode = nil)
    @network.sync

    rotate = ['/', '-', '\\', '|']
    cur = 0

    while !@network.sync_finished?
      $stderr.print "#{@network.status}#{rotate[cur]}\r"

      cur = (cur + 1) % rotate.length

      sleep 0.1
    end

    $stderr.print "#{@network.status}done.\n"
    if mode == :tx
      $stderr.print "Transaction sent.\n\n"
    else
      $stderr.print "Block chain synchronized.\n\n"
    end
  end

  def btc_to_satoshi(btc)
    btc * Rational(10 ** 8)
  end

  def satoshi_to_btc(satoshi)
    Rational(satoshi, 10**8)
  end

  def generate(name)
    return usage "the name \"#{name}\" already exists" if @keys.has_key?(name)

    key = Key.new

    open(@keys_file_name, 'a') do |file|
      file.write "#{name} #{key.to_der_hex_s}\n"
    end

    puts "new Bitcoin address \"#{name}\" generated: #{key.to_address_s}"
  end

  def list
    if @keys.empty?
      puts 'No addresses available'
      return
    end

    puts 'List of available Bitcoin addresses: '
    @keys.each do |name, key|
      puts "    #{name}: #{key.to_address_s}"
    end
  end

  def export(name)
    return usage "an address named #{name} doesn't exist" unless @keys.has_key?(name)

    $stderr.print "Are you sure you want to export private key for \"#{name}\"? (yes/NO): "

    if $stdin.gets.chomp.downcase == 'yes'
      puts @keys[name].to_private_key_s
    end
  end

  def balance
    $stderr.print "loading data ...\r"

    init_network
    wait_for_sync

    puts 'Balances for available Bitcoin addresses: '

    balance = @network.get_balance
    balance.each do |addr, satoshi|
      puts "    #{ addr }: #{ sprintf('%.8f', satoshi_to_btc(satoshi)) } BTC"
    end
  end

  def confirm_send(name, to, amount)
    $stderr.print "Are you sure you want to send\n"
    $stderr.print "    #{sprintf('%.8f', satoshi_to_btc(amount))} BTC\n"
    $stderr.print "from\n    \"#{name}\"\nto\n    \"#{to}\"\n? (yes/no): "

    $stdin.gets.chomp.downcase == 'yes'
  end

  def send(name, to, amount)
    return usage "an address named #{name} doesn't exist" unless @keys.has_key?(name)

    init_network
    wait_for_sync

    if confirm_send(name, to, amount)
      begin
        @network.send(@keys[name], to, amount)
      rescue => e
        warn "bcwallet.rb: #{e}"
        return
      end
      wait_for_sync
    end
  end

  def block(hash)
    init_network
    puts @network.block([hash].pack('H*').reverse)
  end
end

if caller.length == 0
  unless IS_TESTNET
    warn 'WARNING: RUNNING UNDER MAIN NETWORK MODE'
  end

  key_file_name = IS_TESTNET ? 'keys_testnet' : 'keys'
  data_file_name = IS_TESTNET ? 'data_testnet' : 'data'

  bcwallet = BCWallet.new(ARGV, key_file_name, data_file_name)

  bcwallet.run
end