diff --git a/lib/em-bitcoin.rb b/lib/em-bitcoin.rb index c7220b2..abd9855 100644 --- a/lib/em-bitcoin.rb +++ b/lib/em-bitcoin.rb @@ -7,13 +7,6 @@ module EventMachine # Implements the TCP protocol that Bitcoin peers speak to each other. This # module is mixed into both incoming and outgoing connections. # - # - # - # Here is a list of states: - # send_ver, recv_ver, verify_ver - # send_verack, recv_verack - # wait, finish - # # Documentation is here: https://en.bitcoin.it/wiki/Network # # @author Nick Thomas @@ -28,10 +21,6 @@ module EventMachine # The actor for this peer attr_reader :actor - # Randomly-generated 32-bit number allowing us to guarantee we're not - # connecting to ourselves. - attr_reader :connection_nonce - # The list of methods a valid actor will respond to. ACTOR_METHODS = [ :network_name, # Returns a symbol telling us which network to be on @@ -39,33 +28,43 @@ module EventMachine :sub_version, # String specifying custom version string :current_height, # Number of the newest block known to the actor :log, # log(:level, message) - self-evident + :node_nonce, # 32-bit number lets us identify streams to self :connection=, # Called with +self+ to allow actor interaction :ready! # Called when the connection is ready to be used ] # receive_version and receive_verack implementations differ in client & # server, so are implemented there. + + # Simple wrapper around +send_data+ that logs usage + # @param[BTC::Message] packet Packet to send + def send_packet(packet) + data = packet.to_binary_s + log(:info, "Sending #{packet.cmd_sym} message (#{data.length}b)") + log(:debug, data.inspect) + log(:debug, packet.inspect) + send_data(data) + end # Send a 'version' message to the peer. def send_version - log(:info, "Sending version message") - m = build_message(:version, { + :version => BTC::CURRENT_VERSION(actor.network_name), :services => {:node_network => 1}, :timestamp => actor.current_time.to_i, :addr_me => my_netaddr, :addr_you => peer_netaddr, - :nonce => connection_nonce, + :nonce => actor.node_nonce, :sub_version_num => (actor.sub_version || "em-bitcoin"), :start_height => actor.current_height }) - send_data(m.to_binary_s) + send_packet(m) end # Send a 'verack' message to the peer def send_verack log(:info, "Sending verack message") - send_data(build_message(:verack).to_binary_s) + send_packet(build_message(:verack)) end protected @@ -75,7 +74,8 @@ module EventMachine def build_message(command, payload_opts = {}, header_opts = {}) header_opts[:command] = command.to_s header_opts[:magic] ||= BTC::NETWORKS[actor.network_name] - m = BTC::Message.new(:header => header_opts, :payload => payload_opts) + header_opts[:payload] = payload_opts + m = BTC::Message.new(header_opts) end def log(level, data) @@ -85,7 +85,6 @@ module EventMachine def init_state! @data = "" @ready = nil - @connection_nonce = rand(2**32) actor.connection = self # Tell the actor about the connection end @@ -122,7 +121,10 @@ module EventMachine while !finished begin packet = BTC::Message.read(@data) - @data.slice!(0, packet.num_bytes - 1) # Remove data from buffer + used = @data.slice!(0, packet.num_bytes) # Remove data from buf + log(:info, "Read #{packet.cmd_sym} packet (#{used.length}b)") + log(:debug, used.inspect) + log(:debug, packet.inspect) if packet.cmd_sym m = "receive_#{packet.cmd_sym}" if self.respond_to?(m) @@ -133,13 +135,15 @@ module EventMachine end else log(:warn, "Received packet with no command, discarding it") - log(:debug, packet) end + rescue IOError # Not enough data + finished = true rescue EOFError finished = true end end log(:debug, "Leaving receive_data with #{@data.length} bytes in buffer") + log(:debug, @data.inspect) if @data.length > 0 end # Checks whether we can communicate sensibly with a peer of a particular @@ -240,6 +244,10 @@ module EventMachine class BitcoinServer < EM::Connection include BitcoinPeer + def port + my_netaddr.port + end + # @param[Object] actor See the BitcoinPeer#valid_actor? method def initialize(actor) @actor = actor diff --git a/spec/em-bitcoin_spec.rb b/spec/em-bitcoin_spec.rb index ad8178d..f0de26f 100644 --- a/spec/em-bitcoin_spec.rb +++ b/spec/em-bitcoin_spec.rb @@ -5,15 +5,33 @@ require 'em-bitcoin' include ::EM::P class MockActor - attr_accessor :connection + attr_accessor :connection, :network_name, :sub_version, :current_height, + :node_nonce + + def inspect + "#" + end + + def to_s ; inspect ; end - def network_name ; :main ; end - def current_time ; Time.now ; end - def sub_version ; nil ; end - def current_height ; 0 ; end + def initialize(attributes = {}) + self.network_name = :main # defaults + self.current_height = 0 + self.node_nonce = 0 + + attributes.each {|k,v| self.send("#{k}=", v) } + end + + def current_time=(new_time) + @current_time = new_time + end + + def current_time + @current_time || Time.now + end def log(level, msg) - STDERR.puts("#{level}: #{msg}") if $DEBUG + STDERR.puts([name, level, msg].join(": ")) #if $DEBUG end def ready! @@ -24,49 +42,57 @@ class MockActor @ready == true end -end + protected -class MockSignature - attr_reader :lip, :lport, :rip, :rport + attr_accessor :name - def initialize(lip = "127.0.0.1", lport = 12701, rip="127.0.0.2", rport=12702) - @lip, @lport, @rip, @rport = lip, lport, rip, rport - end end - -module EMPMocks - def get_peername - Socket::pack_sockaddr_in(@signature.rport, @signature.rip) - end - - def get_sockname - Socket::pack_sockaddr_in(@signature.lport, @signature.lip) - end - - def send_data(d) - nil # TODO: expectations &c... - end -end - shared_examples_for BitcoinPeer do + end describe BitcoinClient do - before(:each) do - @reactor = stub("reactor") - @sig = MockSignature.new + include EM::Spec - @actor = MockActor.new - @client = BitcoinClient.new(@sig, @actor) - @client.extend(EMPMocks) + def host + "127.0.0.1" end + + def port + @port || 0 + end + + def start_server(actor = @server_actor) + sig = EM::start_server(host, port, EM::P::BitcoinServer, actor) + + @port = Socket::unpack_sockaddr_in(EM::get_sockname(sig))[0] + @server + end + + def start_client(actor = @client_actor) + @client = EM::connect(host, port, EM::P::BitcoinClient, actor) + end + + before(:each) do + @client_actor = MockActor.new(:name => "client") + @server_actor = MockActor.new(:name => "server") + done + end + describe "connection setup" do context "successful" do - it "should accept a valid actor" do - @client.actor.should == @actor - end + it "should connect to a Bitcoin server" do + start_server + start_client + + done - it "should send a version first, accept a verack + version, then send a verack" +# EM::add_periodic_timer(2) do +# @client_actor.should_receive(:ready!).and_return(true) +# @client_actor.should_receive(:connection=).with(@client).and_return(true) +# done +# end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 217329f..6536736 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,5 @@ require 'rspec' +require 'em-spec/rspec' $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) @@ -8,3 +9,4 @@ def binary(str_ary) d.to_i(16).chr end.join("") end +