From 7e31f3976d0bd0c614de4611e345aab48d39fb2b Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 19 May 2011 01:06:54 +0100 Subject: [PATCH] More tests and fixes. Currently fixing Message - see the Version message test --- lib/btc_wire_proto.rb | 105 ++++++++++++++++++----- spec/btc_wire_proto_spec.rb | 161 +++++++++++++++++++++++++++++++++++- spec/spec_helper.rb | 7 ++ 3 files changed, 248 insertions(+), 25 deletions(-) diff --git a/lib/btc_wire_proto.rb b/lib/btc_wire_proto.rb index 2e71da5..ab66619 100644 --- a/lib/btc_wire_proto.rb +++ b/lib/btc_wire_proto.rb @@ -1,9 +1,44 @@ require 'bindata' +require 'ipaddress' + +# Wraps the IPAddress::IPv6 class found in the ipaddress gem to provide easier +# handling of binary-format IP addresses +# @author Nick Thomas +class Ipv6Address < BinData::Primitive + endian :big + + uint128 :address + + def get + if address + if address >= 0xffff00000000 && address <= 0xffffffffffff # v6-mapped v4 + IPAddress::IPv6::Mapped.parse_u128(address) + else # v6 + IPAddress::IPv6.parse_u128(address) + end + else + nil # Nothing set + end + end + + def set(v) + v = IPAddress::IPv6::Mapped.new(v.to_s) if v.is_a?(IPAddress::IPv4) + v = IPAddress(v.to_s) unless v.is_a?(IPAddress::IPv6) + + if v.respond_to?(:to_u128) + address = v.to_u128 + else + raise ArgumentError.new("Can't set #{v.class} to an IPv6Address") + end + end +end + + # Implementation of the BitCoin wire protocol, written using bindata. # Reference: https://en.bitcoin.it/wiki/Protocol_specification # -# @author Nick Thomas module BtcWireProto CURRENT_VERSION = 32100 # Comprehensive list of known networks. The hex values are what you see in @@ -52,9 +87,9 @@ module BtcWireProto # @author Nick Thomas class ServicesMask < BinData::Record endian :little - - bit63 :undefined + bit7 :top_undefined bit1 :node_network + bit56 :undefined end # Structure holding an IP address and port in a slightly unusual format. @@ -64,7 +99,7 @@ module BtcWireProto class NetAddr < BinData::Record endian :big services_mask :services - uint128 :ip # IPv6 address. IPv4 addresses given as IPv6-mapped IPv4 + ipv6_address :ip # IPv6 address. IPv4 addresses given as IPv6-mapped IPv4 uint16 :port end @@ -73,13 +108,14 @@ module BtcWireProto class TimestampedNetAddr < BinData::Record endian :little - uint32 :timestamp + uint32 :timestamp # TODO: Allow this to be set with Ruby native types net_addr :net_addr end # Variable-length integer. This is slightly scary. # @author Nick Thomas class VarInt < BinData::BasePrimitive + register_self def value_to_binary_string(val) val = val.to_i @@ -87,8 +123,8 @@ module BtcWireProto if val < -0xffffffffffffffff # unrepresentable "" elsif val < 0 # 64-bit negative integer - top_32 = (val & 0xffffffff00000000) >> 32 - btm_32 = val & 0x00000000ffffffff + top_32 = ((-val) & 0xffffffff00000000) >> 32 + btm_32 = (-val) & 0x00000000ffffffff [0xff, top_32, btm_32].pack("CVV") elsif val <= 0xfc # 8-bit (almost) positive integer [val].pack("C") @@ -102,8 +138,6 @@ module BtcWireProto end def read_and_return_value(io) - return nil if io.length < 1 - magic = read_uint8(io) if magic <= 0xfc # 8-bit (almost) positive integer magic @@ -119,9 +153,26 @@ module BtcWireProto def sensible_default 0 end - end + + protected - BinData::RegisteredClasses.register("var_int", VarInt) + def read_uint8(io) + io.readbytes(1).unpack("C").at(0) + end + + def read_uint16(io) + io.readbytes(2).unpack("v").at(0) + end + + def read_uint32(io) + io.readbytes(4).unpack("V").at(0) + end + + def read_uint64(io) + top, bottom = io.readbytes(8).unpack("VV") + (top << 32) | bottom + end + end # Variable-length pascal string with a variable-length int specifying the # length. I kid you not. @@ -194,10 +245,10 @@ module BtcWireProto services_mask :services uint64 :timestamp net_addr :addr_me - net_addr :addr_you, :only_if => lambda { version >= 106 } - uint64 :nonce, :only_if => lambda { version >= 106 } - var_str :sub_version, :only_if => lambda { version >= 106 } - uint32 :start_height, :only_if => lambda { version >= 209 } + net_addr :addr_you, :onlyif => lambda { version >= 106 } + uint64 :nonce, :onlyif => lambda { version >= 106 } + var_str :sub_version, :onlyif => lambda { version >= 106 } + uint32 :start_height, :onlyif => lambda { version >= 209 } end # Payload for an addr message in versions earlier than 31402. These are @@ -307,6 +358,13 @@ module BtcWireProto uint32 :reply # See REPLYCODES for possible values end + # Completely empty payload. BinData dies if we don't specify *something* + # in the message payload choices. + # @author Nick Thomas + class NullPayload < BinData::Record + endian :little + end + # A message sent using the p2p network. Signed by a key so you can tell who # sent it - if it's signed by a particular key, then we should apparently # show the message to the user and cease operation until further notice. Fun! @@ -345,8 +403,9 @@ module BtcWireProto # @param[Fixnum,nil] version The protocol version. Setting this affects # the layout of various fields. - def initialize(version = nil) - @version = version || BtcWireProto::CURRENT_VERSION + def initialize_instance(v = nil) + super() + @version = v || BtcWireProto::CURRENT_VERSION end message_hdr :header @@ -365,13 +424,13 @@ module BtcWireProto check_order "checkorder" submit_order "submitorder" alert "alert" + null_payload "null" end - protected - # Works out what the payload looks like based on the MessageHdr struct # and (potentially) the version def payload_choice + puts header.command return header.command if %w{ version inv getdata getblocks getheaders tx block headers alert }.include?(header.command) @@ -383,12 +442,14 @@ module BtcWireProto ) if %w|checkorder submitorder|.include?(header.command) # These commands don't have any payloads - return nil if %w|verack getaddr ping|.include?(header.command) + return "null" if %w|verack getaddr ping|.include?(header.command) || + header.command == "" # Payload has two forms, depending on protocol version. Ugh. return (@version < 31402 ? "addr_pre31402" : "addr_from31402") if - header.command == "addr" - + header.command == "addr" + + raise NotImplementedError.new("Unknown command: #{header.command.inspect}") end end diff --git a/spec/btc_wire_proto_spec.rb b/spec/btc_wire_proto_spec.rb index 5b45f3f..0820f0e 100644 --- a/spec/btc_wire_proto_spec.rb +++ b/spec/btc_wire_proto_spec.rb @@ -3,8 +3,6 @@ require 'btc_wire_proto' # Fixtures data. Taken from: https://en.bitcoin.it/wiki/Protocol_specification -SERVICE_MASK = "\x00\x00\x00\x01" # Sets NODE_NETWORK - include ::BtcWireProto describe ::BtcWireProto do @@ -15,33 +13,181 @@ describe ::BtcWireProto do s = ServicesMask::read("\x00" * 8) # All 64 bits unset s.node_network.should == 0 - s = ServicesMask::read("\x00" * 7 + "\x01") + s = ServicesMask::read(binary(%w{01 00 00 00 00 00 00 00})) s.node_network.should == 1 end end describe NetAddr do + it "should have all fields set to 0 when the input data is all zeroes" do + na = NetAddr::read("\x00" * 26) + + na.services.node_network.should == 0 + na.ip.to_u128.should == 0 + na.port.should == 0 + end + + it "Should allow the Ip field to be set with Ruby native types" do + na = NetAddr::read("\x00" * 26) + mip = IPAddress("::ffff:0.0.0.1") + na.ip = mip + na.ip.to_u128.should == mip.to_u128 + end + + it "should have the fields set appropriately when fed binary data" do + na = NetAddr::read( + binary(%w{ + 01 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 FF FF 0A 00 00 01 20 8D + }) + ) + + na.services.node_network.should == 1 + na.ip.to_s.should == "::ffff:10.0.0.1" + na.port.should == 8333 + end end + + describe TimestampedNetAddr do + it "should leverage NetAddr" do + tna = TimestampedNetAddr::read("\x00" * 30) + tna.net_addr.class.should == BtcWireProto::NetAddr + end + + it "should have all fields set to 0 when the input data is all zeroes" do + tna = TimestampedNetAddr::read("\x00" * 30) + tna.timestamp.should == 0 + end + + it "should have the fields set appropriately when fed binary data" do + tna = TimestampedNetAddr::read( + binary(%w{ + E2 15 10 4D 01 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 FF FF 0A 00 00 01 20 8D + }) + ) + + tna.timestamp.should == 1292899810 + + tna.net_addr.services.node_network.should == 1 + tna.net_addr.ip.to_s.should == "::ffff:10.0.0.1" + tna.net_addr.port.should == 8333 + end end + + describe VarInt do + it "should hold numbers < 0x00000000 in nine bytes" do + bin_minus_1 = "\xff" + "\x00\x00\x00\x00" + "\x01\x00\x00\x00" + a = VarInt::read(bin_minus_1) + a.should == -1 + a.num_bytes.should == 9 + a.to_binary_s.should == bin_minus_1 + + a = VarInt::read("\xff" * 9) + a.should == -(2**64 - 1) + a.num_bytes.should == 9 + a.to_binary_s.should == "\xff" * 9 + end + + it "should hold numbers >= 0x00000000 and < 0x000000fd in one byte" do + a = VarInt::read("\x00") + a.should == 0x00 + a.num_bytes.should == 1 + a.to_binary_s.should == "\x00" + + a = VarInt::read("\xFC") + a.should == 0xFC + a.num_bytes.should == 1 + a.to_binary_s.should == "\xFC" + end + + it "should hold numbers >= 0x000000fd and < 0x00010000 in three bytes" do + a = VarInt::read("\xFD\xFD\x00") + a.should == 0xFD + a.num_bytes.should == 3 + a.to_binary_s.should == "\xFD\xFD\x00" + + a = VarInt::read("\xFD\xFF\xFF") + a.should == 0xFFFF + a.num_bytes.should == 3 + a.to_binary_s.should == "\xFD\xFF\xFF" + end + + it "should hold numbers >= 0x00010000 and < 0xffffffff in five bytes" do + a = VarInt::read("\xFE\x00\x00\x01\x00") + a.should == 0x10000 + a.num_bytes.should == 5 + a.to_binary_s.should == "\xFE\x00\x00\x01\x00" + + a = VarInt::read("\xFE\xFF\xFF\xFF\xFF") + a.should == 0xFFFFFFFF + a.num_bytes.should == 5 + a.to_binary_s.should == "\xFE\xFF\xFF\xFF\xFF" + + end end + describe VarStr do + it "should store string length in a var_int" do + a = VarStr::read("\x04abcd") + a.should == "abcd" + a.num_bytes.should == 5 + a.to_binary_s.should == "\x04abcd" + + a = VarStr::read("\xFD\xFF\xFF" + "A" * 0xFFFF) + a.should == "A" * 0xFFFF + a.num_bytes.should == 0xFFFF + 3 + a.to_binary_s.should == "\xFD\xFF\xFF" + "A" * 0xFFFF + end end + describe InventoryVector do end + describe Sha256 do end + describe TransactionIn do end + describe TransactionOut do end + describe BlockHdr do end # Payloads describe Version do + it "should interpret binary data correctly" do + ver = Version::read(binary(%w{ + 9C 7C 00 00 01 00 00 00 00 00 00 00 E6 15 10 4D 00 00 00 00 + 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF + 0A 00 00 01 DA F6 01 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 FF FF 0A 00 00 02 20 8D DD 9D 20 2C 3A B4 57 13 + 00 55 81 01 00 + })) + + ver.version.should == 31900 + ver.services.node_network.should == 1 + ver.timestamp.should == 1292899814 + ver.addr_me.class.should == NetAddr + ver.addr_you.class.should == NetAddr + ver.nonce.should == 0x1357B43A2C209DDD + ver.sub_version.should == "" + ver.start_height.should == 98645 + end + + it "should exclude some fields by version" do + v = Version::read([1].pack("V") + "\x00" * 42) + v.num_bytes.should == 46 + v = Version::read([106].pack("V") + "\x00" * 77) + v.num_bytes.should == 81 + v = Version::read([209].pack("V") + "\x00" * 81) + v.num_bytes.should == 85 + end end describe AddrPre31402 do @@ -83,6 +229,15 @@ describe ::BtcWireProto do end describe Message do + context "Version message" do + it "should have a Version payload" do + m = Message::new + puts m.inspect + puts m.header.inspect + m.header.command = "version" + v.payload.class.should == Version + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 48562a3..217329f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,10 @@ require 'rspec' $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) + +def binary(str_ary) + str_ary.collect do |d| + raise ArgumentError.new("Bad part") unless d =~ /\A[a-f0-9]{2}\Z/i + d.to_i(16).chr + end.join("") +end