Skip to content

Commit

Permalink
fix: handle single-byte strings in RLP encoding/decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelarogbonlo committed Nov 28, 2024
1 parent 7fa2244 commit 88c07c7
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 36 deletions.
36 changes: 25 additions & 11 deletions lib/eth/rlp/decoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,45 @@ module Decoder
# @raise [Eth::Rlp::DecodingError] if the input string does not end after
# the root item.
def perform(rlp)
rlp = Util.hex_to_bin rlp if Util.hex? rlp
rlp = Util.str_to_bytes rlp
input = case rlp
when Rlp::Data
rlp
when String
# Handle hex strings
if Util.hex?(rlp)
Util.hex_to_bin(rlp)
else
rlp.dup.force_encoding(Encoding::ASCII_8BIT)
end
else
raise TypeError, "RLP input must be String or Rlp::Data"
end

begin
item, next_start = consume_item rlp, 0
item, next_start = consume_item(input, 0)
rescue Exception => e
raise DecodingError, "Cannot decode rlp string: #{e}"
end
raise DecodingError, "RLP string ends with #{rlp.size - next_start} superfluous bytes" if next_start != rlp.size
return item

# Check if we consumed the whole input
if next_start != input.bytesize
raise DecodingError, "RLP string ends with #{input.bytesize - next_start} superfluous bytes"
end

item
end

private

# Consume an RLP-encoded item from the given start.
def consume_item(rlp, start)
t, l, s = consume_length_prefix rlp, start
consume_payload rlp, s, t, l
type, length, pos = consume_length_prefix(rlp, start)
consume_payload(rlp, pos, type, length)
end

# Consume an RLP length prefix at the given position.
def consume_length_prefix(rlp, start)
b0 = rlp[start].ord
if b0 < Constant::PRIMITIVE_PREFIX_OFFSET

# single byte
[:str, 1, start]
elsif b0 < Constant::PRIMITIVE_PREFIX_OFFSET + Constant::SHORT_LENGTH_LIMIT
Expand All @@ -71,7 +86,6 @@ def consume_length_prefix(rlp, start)
raise DecodingError, "Long string prefix used for short string" if l < Constant::SHORT_LENGTH_LIMIT
[:str, l, start + 1 + ll]
elsif b0 < Constant::LIST_PREFIX_OFFSET + Constant::SHORT_LENGTH_LIMIT

# short list
[:list, b0 - Constant::LIST_PREFIX_OFFSET, start + 1]
else
Expand Down Expand Up @@ -111,4 +125,4 @@ def consume_payload(rlp, start, type, length)
end
end
end
end
end
37 changes: 25 additions & 12 deletions lib/eth/rlp/encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ module Encoder
# @raise [Eth::Rlp::SerializationError] if the serialization fails.
def perform(obj)
item = Sedes.infer(obj).serialize(obj)
result = encode_raw item
result = encode_raw(item)
# Ensure result is always Rlp::Data
result.is_a?(Rlp::Data) ? result : Rlp::Data.new(result.force_encoding(Encoding::ASCII_8BIT))
end

private
Expand All @@ -43,30 +45,41 @@ def encode_raw(item)
return item if item.instance_of? Rlp::Data
return encode_primitive item if Util.primitive? item
return encode_list item if Util.list? item
raise EncodingError "Cannot encode object of type #{item.class.name}"
raise EncodingError, "Cannot encode object of type #{item.class.name}"
end

# Encodes a single primitive.
# Encodes a primitive (string or number).
def encode_primitive(item)
return Util.str_to_bytes item if item.size == 1 && item.ord < Constant::PRIMITIVE_PREFIX_OFFSET
payload = Util.str_to_bytes item
prefix = length_prefix payload.size, Constant::PRIMITIVE_PREFIX_OFFSET
"#{prefix}#{payload}"
return item if item.is_a?(Rlp::Data)

bytes = case item
when String
item.dup.force_encoding(Encoding::ASCII_8BIT)
else
item.to_s.force_encoding(Encoding::ASCII_8BIT)
end

if bytes.size == 1 && bytes.getbyte(0) < Constant::PRIMITIVE_PREFIX_OFFSET
Rlp::Data.new(bytes)
else
prefix = length_prefix(bytes.size, Constant::PRIMITIVE_PREFIX_OFFSET)
Rlp::Data.new("#{prefix}#{bytes}")
end
end

# Encodes a single list.
def encode_list(list)
payload = list.map { |item| encode_raw item }.join
prefix = length_prefix payload.size, Constant::LIST_PREFIX_OFFSET
"#{prefix}#{payload}"
payload = list.map { |item| encode_raw(item) }.join
prefix = length_prefix(payload.size, Constant::LIST_PREFIX_OFFSET)
Rlp::Data.new("#{prefix}#{payload}")
end

# Determines a length prefix.
def length_prefix(length, offset)
if length < Constant::SHORT_LENGTH_LIMIT
(offset + length).chr
elsif length < Constant::LONG_LENGTH_LIMIT
length_string = Util.int_to_big_endian length
length_string = Util.int_to_big_endian(length)
length_len = (offset + Constant::SHORT_LENGTH_LIMIT - 1 + length_string.size).chr
"#{length_len}#{length_string}"
else
Expand All @@ -75,4 +88,4 @@ def length_prefix(length, offset)
end
end
end
end
end
22 changes: 9 additions & 13 deletions lib/eth/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,19 @@ def keccak256(str)
# @return [String] a hexa-decimal string.
# @raise [TypeError] if value is not a string.
def bin_to_hex(bin)
raise TypeError, "Value must be an instance of String" unless bin.instance_of? String
bin.unpack("H*").first
return nil unless bin

# Handle both String and Rlp::Data
bin_str = bin.is_a?(String) ? bin : bin.to_s
bin_str.unpack('H*')[0]
end

# Packs a hexa-decimal string into a binary string. Also works with
# `0x`-prefixed strings.
#
# @param hex [String] a hexa-decimal string to be packed.
# @return [String] a packed binary string.
# @raise [TypeError] if value is not a string or string is not hex.
def hex_to_bin(hex)
raise TypeError, "Value must be an instance of String" unless hex.instance_of? String
hex = remove_hex_prefix hex
raise TypeError, "Non-hexadecimal digit found" unless hex? hex
[hex].pack("H*")
return hex unless hex?(hex)
hex = hex.gsub(/\A0x/, '')
[hex].pack('H*')
end

# Prefixes a hexa-decimal string with `0x`.
#
# @param hex [String] a hex-string to be prefixed.
Expand Down
9 changes: 9 additions & 0 deletions spec/eth/rlp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,14 @@
expect(encoded_again).to eq Util.hex_to_bin rlp
end
end

it "properly handles single-byte strings" do
test_cases = ["a", "b", "1", "\x01"]
test_cases.each do |input|
encoded = Eth::Rlp.encode(input)
decoded = Eth::Rlp.decode(encoded)
expect(decoded).to eq(input)
end
end
end
end

0 comments on commit 88c07c7

Please sign in to comment.