diff --git a/README b/README new file mode 100644 index 0000000..3883125 --- /dev/null +++ b/README @@ -0,0 +1,20 @@ +For now: See doc/AIMS + +Currently, nothing is interesting. + +Here's a general idea of the order in which i'm going to be implementing things. +Note that nothing is set in stone here. Also, parallelism is to be expected as +I get bored of one thing and so start on another. + + * Basic em-bitcoin functionality, dragging along btc_wire_proto as I go + - Build the full block chain from scratch + - Be able to send transactions to others + - Wallets + * HTTP interface for wallets, users + - Client-side transaction signing + * Bitcoin RPC interface for interop with existing management tools + * Simple market for registered users + - Multiple currency support (GBP to start, then euro, then dollar) + - Integration for various put-cash-into-the-market services (wire transfer, to start) + +Once those bits are in, polish them 'till they gleam. Then start on the other bits. diff --git a/lib/em-bitcoin.rb b/lib/em-bitcoin.rb index 5c73fdd..c7220b2 100644 --- a/lib/em-bitcoin.rb +++ b/lib/em-bitcoin.rb @@ -23,23 +23,70 @@ module EventMachine end PE = ProtocolError + BTC = ::BtcWireProto + + # 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 + :current_time, # Time instance specifying the current time + :sub_version, # String specifying custom version string + :current_height, # Number of the newest block known to the actor :log, # log(:level, message) - self-evident :connection=, # Called with +self+ to allow actor interaction :ready! # Called when the connection is ready to be used ] - def log(level, data) - @actor.log(level, data) + # receive_version and receive_verack implementations differ in client & + # server, so are implemented there. + + # Send a 'version' message to the peer. + def send_version + log(:info, "Sending version message") + + m = build_message(:version, { + :services => {:node_network => 1}, + :timestamp => actor.current_time.to_i, + :addr_me => my_netaddr, + :addr_you => peer_netaddr, + :nonce => connection_nonce, + :sub_version_num => (actor.sub_version || "em-bitcoin"), + :start_height => actor.current_height + }) + send_data(m.to_binary_s) end + # Send a 'verack' message to the peer + def send_verack + log(:info, "Sending verack message") + send_data(build_message(:verack).to_binary_s) + end + protected + # Puts together a message from the details given. Common fields, like + # magic and checksums, are filled in. + 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) + end + + def log(level, data) + actor.log(level, data) + end + def init_state! @data = "" @ready = nil - @actor.connection = self # Tell the actor about the connection + @connection_nonce = rand(2**32) + actor.connection = self # Tell the actor about the connection end # Checks the current actor object to see if it is valid or not. @@ -59,7 +106,7 @@ module EventMachine ACTOR_METHODS.each do |m| return [false, "Actor doesn't implement all #{m}"] unless - @actor.respond_to?(m) + actor.respond_to?(m) end true @@ -74,7 +121,7 @@ module EventMachine finished = false while !finished begin - packet = BtcWireProto::Message.read(@data) + packet = BTC::Message.read(@data) @data.slice!(0, packet.num_bytes - 1) # Remove data from buffer if packet.cmd_sym m = "receive_#{packet.cmd_sym}" @@ -94,19 +141,36 @@ module EventMachine end log(:debug, "Leaving receive_data with #{@data.length} bytes in buffer") end - - # receive_version and receive_verack implementations differ in client & - # server, so are implemented there. - # Send a 'version' message to the peer. - def send_version - # TODO + # Checks whether we can communicate sensibly with a peer of a particular + # version. At the moment, we're fairly liberal about who we try to talk to + # @param[Fixnum] their_version Version of the peer under question + # @return[Boolean] can we communicate with a peer advertising the version? + def can_talk_version?(their_version) + their_version >= 31402 end - # Send a 'verack' message to the peer - def send_verack - # TODO + # @return[BTC::NetAddr] wire represenation of our own network address + def my_netaddr + sockaddr = get_sockname + raise PE.new("Unable to determine own NetAddr") unless sockaddr + port, ip = Socket::unpack_sockaddr_in(sockaddr) + BTC::NetAddr.new( + :services => {:node_network => 1}, :ip => ip, :port => port + ) end + + # FIXME: what does node_network mean here? + # @return[BTC::NetAddr] wire representation of our peer's network address + def peer_netaddr + sockaddr = get_peername + raise PE.new("Unable to determine peer NetAddr") unless sockaddr + port, ip = Socket::unpack_sockaddr_in(sockaddr) + BTC::NetAddr.new( + :services => {:node_network => 1}, :ip => ip, :port => port + ) + end + end # EventMachine protocol class that handles an *outgoing* connection to @@ -122,7 +186,6 @@ module EventMachine # @param[Object] actor See the BitcoinPeer#valid_actor? def initialize(actor) - super @actor = actor result, msg = valid_actor? raise ArgumentError.new("Invalid actor: #{msg}") unless result @@ -154,10 +217,16 @@ module EventMachine def receive_version(p) raise PE.new("Received version inappropriately") unless @ready == :version_sent - log(:info, "Peer tells us its version is #{p.payload.version}") - send_verack - @ready = true - @actor.ready! + + if can_talk_version?(p.payload.version) + log(:info, "Valid peer, version is #{p.payload.version}") + send_verack + @ready = true + actor.ready! + else + log(:info, "Bad peer, can't talk version #{p.payload.version}") + unbind + end end end @@ -173,23 +242,37 @@ module EventMachine # @param[Object] actor See the BitcoinPeer#valid_actor? method def initialize(actor) - super @actor = actor result, msg = valid_actor? raise ArgumentError.new("Invalid actor: #{msg}") unless result init_state! end - - # We should receive this as the very first message on the wire + # We should receive this as the very first message on the wire. + # If we're prepared ot talk to a client with this version, we send a + # verack message followed by our own version and wait for a verack to be + # received. def receive_version(p) - # TODO + raise PE.new("Received version inappropriately") unless @ready.nil? + if can_talk_version?(p.payload.version) + log(:info, "Peer version #{p.payload.version}") + send_verack + send_version + @ready = :version_sent + else + log(:info, "Bad peer - can't talk version #{p.payload.version}") + unbind + end end - # We should only receive this after sending our own version + # We should only receive this after sending our own version. If the client + # doesn't like our version, it'll close the connection. def receive_verack(p) - # TODO + raise PE.new("Received verack inappropriately") unless + @ready == :version_sent + @ready = true + actor.ready! end end diff --git a/spec/em-bitcoin_spec.rb b/spec/em-bitcoin_spec.rb new file mode 100644 index 0000000..ad8178d --- /dev/null +++ b/spec/em-bitcoin_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'em-bitcoin' + + +include ::EM::P + +class MockActor + attr_accessor :connection + + def network_name ; :main ; end + def current_time ; Time.now ; end + def sub_version ; nil ; end + def current_height ; 0 ; end + + def log(level, msg) + STDERR.puts("#{level}: #{msg}") if $DEBUG + end + + def ready! + @ready = true + end + + def ready? + @ready == true + end + +end + +class MockSignature + attr_reader :lip, :lport, :rip, :rport + + 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 + + @actor = MockActor.new + @client = BitcoinClient.new(@sig, @actor) + @client.extend(EMPMocks) + end + describe "connection setup" do + context "successful" do + it "should accept a valid actor" do + @client.actor.should == @actor + end + + it "should send a version first, accept a verack + version, then send a verack" + end + end + + +end + +describe BitcoinServer do +end +