More tests and fixes. Currently fixing Message - see the Version message test

This commit is contained in:
Nick Thomas
2011-05-19 01:06:54 +01:00
parent f3f89283e2
commit 7e31f3976d
3 changed files with 248 additions and 25 deletions

View File

@@ -1,9 +1,44 @@
require 'bindata' 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 <nick@lupine.me.uk>
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. # Implementation of the BitCoin wire protocol, written using bindata.
# Reference: https://en.bitcoin.it/wiki/Protocol_specification # Reference: https://en.bitcoin.it/wiki/Protocol_specification
# #
# @author Nick Thomas <nick@lupine.me.uk # @author Nick Thomas <nick@lupine.me.uk>
module BtcWireProto module BtcWireProto
CURRENT_VERSION = 32100 CURRENT_VERSION = 32100
# Comprehensive list of known networks. The hex values are what you see in # Comprehensive list of known networks. The hex values are what you see in
@@ -52,9 +87,9 @@ module BtcWireProto
# @author Nick Thomas <nick@lupine.me.uk> # @author Nick Thomas <nick@lupine.me.uk>
class ServicesMask < BinData::Record class ServicesMask < BinData::Record
endian :little endian :little
bit7 :top_undefined
bit63 :undefined
bit1 :node_network bit1 :node_network
bit56 :undefined
end end
# Structure holding an IP address and port in a slightly unusual format. # Structure holding an IP address and port in a slightly unusual format.
@@ -64,7 +99,7 @@ module BtcWireProto
class NetAddr < BinData::Record class NetAddr < BinData::Record
endian :big endian :big
services_mask :services 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 uint16 :port
end end
@@ -73,13 +108,14 @@ module BtcWireProto
class TimestampedNetAddr < BinData::Record class TimestampedNetAddr < BinData::Record
endian :little endian :little
uint32 :timestamp uint32 :timestamp # TODO: Allow this to be set with Ruby native types
net_addr :net_addr net_addr :net_addr
end end
# Variable-length integer. This is slightly scary. # Variable-length integer. This is slightly scary.
# @author Nick Thomas <nick@lupine.me.uk> # @author Nick Thomas <nick@lupine.me.uk>
class VarInt < BinData::BasePrimitive class VarInt < BinData::BasePrimitive
register_self
def value_to_binary_string(val) def value_to_binary_string(val)
val = val.to_i val = val.to_i
@@ -87,8 +123,8 @@ module BtcWireProto
if val < -0xffffffffffffffff # unrepresentable if val < -0xffffffffffffffff # unrepresentable
"" ""
elsif val < 0 # 64-bit negative integer elsif val < 0 # 64-bit negative integer
top_32 = (val & 0xffffffff00000000) >> 32 top_32 = ((-val) & 0xffffffff00000000) >> 32
btm_32 = val & 0x00000000ffffffff btm_32 = (-val) & 0x00000000ffffffff
[0xff, top_32, btm_32].pack("CVV") [0xff, top_32, btm_32].pack("CVV")
elsif val <= 0xfc # 8-bit (almost) positive integer elsif val <= 0xfc # 8-bit (almost) positive integer
[val].pack("C") [val].pack("C")
@@ -102,8 +138,6 @@ module BtcWireProto
end end
def read_and_return_value(io) def read_and_return_value(io)
return nil if io.length < 1
magic = read_uint8(io) magic = read_uint8(io)
if magic <= 0xfc # 8-bit (almost) positive integer if magic <= 0xfc # 8-bit (almost) positive integer
magic magic
@@ -119,9 +153,26 @@ module BtcWireProto
def sensible_default def sensible_default
0 0
end 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 # Variable-length pascal string with a variable-length int specifying the
# length. I kid you not. # length. I kid you not.
@@ -194,10 +245,10 @@ module BtcWireProto
services_mask :services services_mask :services
uint64 :timestamp uint64 :timestamp
net_addr :addr_me net_addr :addr_me
net_addr :addr_you, :only_if => lambda { version >= 106 } net_addr :addr_you, :onlyif => lambda { version >= 106 }
uint64 :nonce, :only_if => lambda { version >= 106 } uint64 :nonce, :onlyif => lambda { version >= 106 }
var_str :sub_version, :only_if => lambda { version >= 106 } var_str :sub_version, :onlyif => lambda { version >= 106 }
uint32 :start_height, :only_if => lambda { version >= 209 } uint32 :start_height, :onlyif => lambda { version >= 209 }
end end
# Payload for an addr message in versions earlier than 31402. These are # 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 uint32 :reply # See REPLYCODES for possible values
end end
# Completely empty payload. BinData dies if we don't specify *something*
# in the message payload choices.
# @author Nick Thomas <nick@lupine.me.uk>
class NullPayload < BinData::Record
endian :little
end
# A message sent using the p2p network. Signed by a key so you can tell who # 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 # 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! # 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 # @param[Fixnum,nil] version The protocol version. Setting this affects
# the layout of various fields. # the layout of various fields.
def initialize(version = nil) def initialize_instance(v = nil)
@version = version || BtcWireProto::CURRENT_VERSION super()
@version = v || BtcWireProto::CURRENT_VERSION
end end
message_hdr :header message_hdr :header
@@ -365,13 +424,13 @@ module BtcWireProto
check_order "checkorder" check_order "checkorder"
submit_order "submitorder" submit_order "submitorder"
alert "alert" alert "alert"
null_payload "null"
end end
protected
# Works out what the payload looks like based on the MessageHdr struct # Works out what the payload looks like based on the MessageHdr struct
# and (potentially) the version # and (potentially) the version
def payload_choice def payload_choice
puts header.command
return header.command if %w{ return header.command if %w{
version inv getdata getblocks getheaders tx block headers alert version inv getdata getblocks getheaders tx block headers alert
}.include?(header.command) }.include?(header.command)
@@ -383,12 +442,14 @@ module BtcWireProto
) if %w|checkorder submitorder|.include?(header.command) ) if %w|checkorder submitorder|.include?(header.command)
# These commands don't have any payloads # 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. # Payload has two forms, depending on protocol version. Ugh.
return (@version < 31402 ? "addr_pre31402" : "addr_from31402") if return (@version < 31402 ? "addr_pre31402" : "addr_from31402") if
header.command == "addr" header.command == "addr"
raise NotImplementedError.new("Unknown command: #{header.command.inspect}")
end end
end end

View File

@@ -3,8 +3,6 @@ require 'btc_wire_proto'
# Fixtures data. Taken from: https://en.bitcoin.it/wiki/Protocol_specification # Fixtures data. Taken from: https://en.bitcoin.it/wiki/Protocol_specification
SERVICE_MASK = "\x00\x00\x00\x01" # Sets NODE_NETWORK
include ::BtcWireProto include ::BtcWireProto
describe ::BtcWireProto do describe ::BtcWireProto do
@@ -15,33 +13,181 @@ describe ::BtcWireProto do
s = ServicesMask::read("\x00" * 8) # All 64 bits unset s = ServicesMask::read("\x00" * 8) # All 64 bits unset
s.node_network.should == 0 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 s.node_network.should == 1
end end
end end
describe NetAddr do 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 end
describe TimestampedNetAddr do 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 end
describe VarInt do 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 end
describe VarStr do 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 end
describe InventoryVector do describe InventoryVector do
end end
describe Sha256 do describe Sha256 do
end end
describe TransactionIn do describe TransactionIn do
end end
describe TransactionOut do describe TransactionOut do
end end
describe BlockHdr do describe BlockHdr do
end end
# Payloads # Payloads
describe Version do 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 end
describe AddrPre31402 do describe AddrPre31402 do
@@ -83,6 +229,15 @@ describe ::BtcWireProto do
end end
describe Message do 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
end end

View File

@@ -1,3 +1,10 @@
require 'rspec' require 'rspec'
$: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) $: << 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