More tests and fixes. Currently fixing Message - see the Version message test
This commit is contained in:
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user