Remove BtcWireProto from this repository - it's split out now

This commit is contained in:
Nick Thomas
2011-06-04 23:00:35 +01:00
parent d1c433b92c
commit 3d7b3dd329
2 changed files with 0 additions and 738 deletions

View File

@@ -1,456 +0,0 @@
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.
# Reference: https://en.bitcoin.it/wiki/Protocol_specification
#
# @author Nick Thomas <nick@lupine.me.uk>
module BtcWireProto
CURRENT_VERSION = 32100
# Comprehensive list of known networks. The hex values are what you see in
# MessageHdr#magic and the symbols are their known friendly names.
NETWORKS = {
:testnet => 0xDAB5BFFA,
0xDAB5BFFA => :testnet,
:main => 0xD9B4BEF9,
0xD9B4BEF9 => :main
}
# Comprehensive list of known inventory vector types.
INV_VEC_TYPES = {
0 => :error,
:error => 0,
1 => :msg_tx,
:msg_tx => 1,
2 => :msg_block,
:msg_block => 2
}
# Used in Reply messages
REPLY_CODES = {
0 => :success,
:success => 0,
1 => :wallet_error,
:wallet_error => 1,
2 => :denied,
:denied => 2
}
# Only Alert messages signed by this key are valid.
# This is an ECDSA public key (FIXME: in what format?)
ALERT_PUBKEY = "04fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b" +
"28a49e0ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7" +
"c5ecbcd68284"
## Components of payloads ##
# Bitmask advertising various capabilities of the node.
# @author Nick Thomas <nick@lupine.me.uk>
class ServicesMask < BinData::Record
endian :little
bit7 :top_undefined
bit1 :node_network
bit56 :undefined
end
# Structure holding an IP address and port in a slightly unusual format.
# This one is big-endian - everything else is little-endian.
#
# @author Nick Thomas <nick@lupine.me.uk>
class NetAddr < BinData::Record
endian :big
services_mask :services
ipv6_address :ip # IPv6 address. IPv4 addresses given as IPv6-mapped IPv4
uint16 :port
end
# Like a NetAddr but with a timestamp to boot.
# @author Nick Thomas <nick@lupine.me.uk>
class TimestampedNetAddr < BinData::Record
endian :little
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 <nick@lupine.me.uk>
class VarInt < BinData::BasePrimitive
register_self
def value_to_binary_string(val)
val = val.to_i
if val < -0xffffffffffffffff # unrepresentable
""
elsif val < 0 # 64-bit negative integer
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")
elsif val <= 0xffff # 16-bit positive integer
[0xfd, val].pack("Cv")
elsif val <= 0xffffffff # 32-bit positive integer
[0xfe, val].pack("CV")
else # We can't represent this, whatever it is
""
end
end
def read_and_return_value(io)
magic = read_uint8(io)
if magic <= 0xfc # 8-bit (almost) positive integer
magic
elsif magic == 0xfd # 16-bit positive integer
read_uint16(io)
elsif magic == 0xfe # 32-bit positive integer
read_uint32(io)
elsif magic == 0xff # 64-bit negative integer
-(read_uint64(io))
end
end
def sensible_default
0
end
protected
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.
# @author Nick Thomas <nick@lupine.me.uk>
class VarStr < BinData::Primitive
endian :little
var_int :len, :value => lambda { data.length }
string :data, :read_length => :len
def get ; self.data ; end
def set(v) ; self.data = v ; end
end
class InventoryVector < BinData::Record
endian :little
uint32 :type # For values, see INV_VEC_TYPES
string :iv_hash, :length => 32
end
# Simple class wrapping raw SHA256 data. Might have utility methods later.
# @author Nick Thomas <nick@lupine.me.uk>
class Sha256 < BinData::Record
string :data, :length => 32 # Raw SHA256 data
end
SHA256 = Sha256
# @author Nick Thomas <nick@lupine.me.uk>
class TransactionIn < BinData::Record
endian :little
sha256 :po_hash
uint32 :po_index
var_str :signature_script # Script for confirming transaction authorisation
uint32 :sequence # Version of this record.
end
# @author Nick Thomas <nick@lupine.me.uk>
class TransactionOut < BinData::Record
endian :little
uint64 :txout_value
var_str :pk_script # Script containing conditions to claim to transaction
end
# Header for a block.
# @author Nick Thomas <nick@lupine.me.uk>
class BlockHdr < BinData::Record
endian :little
uint32 :version
sha256 :prev_block
sha256 :merkle_root
uint32 :timestamp
uint32 :difficulty
uint32 :nonce
var_int :txn_count
end
## Payloads ##
# Payload for a version message
# @author Nick Thomas <nick@lupine.me.uk>
class Version < BinData::Record
endian :little
uint32 :version
services_mask :services
uint64 :timestamp
net_addr :addr_me
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
# used to get a list of peers to interact with.
# @author Nick Thomas <nick@lupine.me.uk>
class AddrPre31402 < BinData::Record
endian :little
var_int :addr_count
array :addrs, :type => :net_addr,
:read_until => lambda { index == addr_count - 1 }
end
# Payload for an addr message in versions later than 31402. A timestamp was
# added to the list of addresses, but otherwise it's the same as AddrPre31402
# @author Nick Thomas <nick@lupine.me.uk>
class AddrFrom31402 < BinData::Record
endian :little
var_int :addr_count
array :timestamped_addrs, :type => :timestamped_net_addr,
:read_until => lambda { index == addr_count - 1 }
end
# Payload for a getdata or inv message. This lets the peer advertise the
# various objects it has knowledge of.
# @author Nick Thomas <nick@lupine.me.uk>
class Inventory < BinData::Record
endian :little
var_int :iv_count
array :items, :type => :inventory_vector,
:read_until => lambda { index == iv_count - 1 }
end
# Payload for a getblocks or getheaders message. Specifies a set of blocks
# that the sender wants details of.
# @author Nick thomas <nick@lupine.me.uk>
class BlockSpec < BinData::Record
endian :little
uint32 :version
var_int :start_count
array :hash_start, :type => :sha256,
:read_until => lambda { index == start_count - 1 }
# Hash of the last desired block, or 0 to get as many as possible (max: 500)
sha256 :hash_stop, :length => 32
end
# A transaction. This contains a number of transactions 'in', and 'out'.
# @author Nick Thomas <nick@lupine.me.uk>
class Transaction < BinData::Record
endian :little
uint32 :version
var_int :tx_in_count
array :transactions_in, :type => :transaction_in,
:read_until => lambda { index == tx_in_count - 1 }
var_int :tx_out_count
array :transactions_out, :type => :transaction_out,
:read_until => lambda { index == tx_out_count - 1 }
uint32 :lock_time
end
# Details about a particular block. Returned in response to a block request
# @author Nick Thomas <nick@lupine.me.uk>
class Block < BinData::Record
endian :little
block_hdr :header
array :txns, :type => :transaction,
:read_until => lambda { index == header.txn_count - 1 }
end
# Headers payloads are returned in response to a getheaders request.
# Limit of 2,000 entries per message.
# @author Nick Thomas <nick@lupine.me.uk>
class Headers < BinData::Record
endian :little
var_int :hdr_count
array :block_hdrs, :type => :block_hdr,
:read_until => lambda { index == hdr_count - 1 }
end
# For now, we don't support CheckOrder requests at all. Protocol documentation
# is lacking! FIXME
# @author Nick Thomas <nick@lupine.me.uk>
class CheckOrder < BinData::Record
endian :little
end
# We don't support SubmitOrder replies either. Receiving either of these will
# actually break the stream, since we don't even know how long they are. FIXME
# @author Nick Thomas <nick@lupine.me.uk>
class SubmitOrder < BinData::Record
endian :little
end
# Used as a response to a CheckOrder request.
# @author Nick Thomas <nick@lupine.me.uk>
class Reply < BinData::Record
endian :little
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 <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
# 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!
# @author Nick Thomas <nick@lupine.me.uk>
class Alert < BinData::Record
endian :little
var_str :message
var_str :signature
end
## Top-level message format ##
# Found at the start of all Bitcoin messages.
# @author Nick Thomas <nick@lupine.me.uk>
class MessageHdr < BinData::Record
endian :little
uint32 :magic
string :command, :length => 12
uint32 :payload_len
uint32 :checksum, :onlyif => :has_checksum?
protected
# version and verack messages don't have a checksum. The rest do.
# @return[Boolean] does this message header have a checksum field or not?
def has_checksum?
!%w|version verack|.include?(command.strip)
end
end
# Everything on the wire is a Message.
# @author Nick Thomas <nick@lupine.me.uk>
class Message < BinData::Record
# @param[Fixnum,nil] version The protocol version. Setting this affects
# the layout of various fields.
def initialize_instance(v = nil)
super()
@version = v || BtcWireProto::CURRENT_VERSION
end
message_hdr :header
choice :payload, :selection => :payload_choice do
version "version"
addr_pre31402 "addr_pre31402"
addr_from31402 "addr_from31402"
inventory "inv"
inventory "getdata"
block_spec "getblocks"
block_spec "getheaders"
transaction "tx"
block "block"
headers "headers"
check_order "checkorder"
submit_order "submitorder"
alert "alert"
null_payload "null"
end
# Works out what the payload looks like based on the MessageHdr struct
# and (potentially) the version
def payload_choice
cmd = header.command.to_s.strip
return cmd if %w{
version inv getdata getblocks getheaders tx block headers alert
}.include?(cmd)
# We can't parse these yet, and so we don't know where in the stream the
# next message starts. So all we can do is throw an error
raise NotImplementedError.new(
"Received unsupported command #{cmd}"
) if %w|checkorder submitorder|.include?(cmd)
# These commands don't have any payloads
return "null" if %w|verack getaddr ping|.include?(cmd) || cmd == ""
# Payload has two forms, depending on protocol version. Ugh.
return (@version < 31402 ? "addr_pre31402" : "addr_from31402") if
cmd == "addr"
raise NotImplementedError.new("Unknown command: #{cmd}")
end
end
end

View File

@@ -1,282 +0,0 @@
require 'spec_helper'
require 'btc_wire_proto'
# Fixtures data. Taken from: https://en.bitcoin.it/wiki/Protocol_specification
include ::BtcWireProto
describe ::BtcWireProto do
# Payload fragments
describe ServicesMask do
it "should have node_network set to false when its bit is 0" do
s = ServicesMask::read("\x00" * 8) # All 64 bits unset
s.node_network.should == 0
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
end
describe AddrFrom31402 do
end
describe Inventory do
end
describe BlockSpec do
end
describe Transaction do
end
describe Block do
end
describe Headers do
end
describe CheckOrder do
end
describe SubmitOrder do
end
describe Reply do
end
describe Alert do
end
# Messages
describe MessageHdr do
end
describe Message do
context "Version message" do
it "should have a Version payload" do
m = Message::new(:header => {:command => 'version'})
m.payload.selection.should == "version"
end
it "should parse binary data correctly" do
m = Message::read(binary(%w{
F9 BE B4 D9 76 65 72 73 69 6F 6E 00 00 00 00 00 55 00 00
00 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
}))
m.header.magic.should == BtcWireProto::NETWORKS[:main]
m.header.command.should == "version\x00\x00\x00\x00\x00"
m.header.payload_len.should == 85
m.header.has_parameter?(:checksum).should be_false
m.payload.version.should == 31900
m.payload.services.node_network.should == 1
m.payload.timestamp.should == 1292899814
m.payload.addr_me.class.should == NetAddr
m.payload.addr_you.class.should == NetAddr
m.payload.nonce.should == 0x1357B43A2C209DDD
m.payload.sub_version.should == ""
m.payload.start_height.should == 98645
end
end
context "Verack message" do
it "should have no payload" do
m = Message.new(:header => {:command => "verack"})
m.payload.selection.should == "null"
end
it "should parse the binary data correctly" do
m = Message::read(binary(%w{
F9 BE B4 D9 76 65 72 61 63 6B 00 00 00 00 00 00 00 00 00 00
}))
m.header.magic.should == BtcWireProto::NETWORKS[:main]
m.header.command.should == "verack\x00\x00\x00\x00\x00\x00"
m.header.payload_len.should == 0
m.header.has_parameter?(:chcksum).should be_false
end
end
end
end