Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth/abi: enable packed encoding #211

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ group :test, :development do
gem "bundler", "~> 2.2"
gem "codecov", "~> 0.6"
gem "pry", "~> 0.14"
gem "rake", "~> 13.0"
gem "rdoc", "~> 6.4"
gem "rspec", "~> 3.11"
gem "rufo", "~> 0.13"
gem "rake", "~> 13.2"
gem "rdoc", "~> 6.7"
gem "rspec", "~> 3.13"
gem "rufo", "~> 0.18"
gem "simplecov", "~> 0.21"
gem "yard", "~> 0.9"
end
Expand Down
48 changes: 41 additions & 7 deletions lib/eth/abi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,55 @@ class ValueOutOfBounds < StandardError; end
#
# @param types [Array] types to be ABI-encoded.
# @param args [Array] values to be ABI-encoded.
# @param packed [Boolean] use custom packed encoding (default: false).
# @return [String] the encoded ABI data.
def encode(types, args)
def encode(types, args, packed = false)
types = [types] unless types.instance_of? Array
args = [args] unless args.instance_of? Array

# parse all types
parsed_types = types.map { |t| Type === t ? t : Type.parse(t) }

# prepare the "head"
head_size = (0...args.size)
.map { |i| parsed_types[i].size or 32 }
.map { |i|
if packed
parsed_types[i].sub_type.to_i / 8
else
parsed_types[i].size or 32
end
}
.reduce(0, &:+)
head, tail = "", ""

# encode types and arguments
args.each_with_index do |arg, i|
if parsed_types[i].dynamic?
head += Abi::Encoder.type(Type.size_type, head_size + tail.size)
tail += Abi::Encoder.type(parsed_types[i], arg)
if packed
head += Abi::Encoder.type(parsed_types[i], arg, packed)
elsif parsed_types[i].dynamic?
head += Abi::Encoder.type(Type.size_type, head_size + tail.size, packed)
tail += Abi::Encoder.type(parsed_types[i], arg, packed)
else
head += Abi::Encoder.type(parsed_types[i], arg)
head += Abi::Encoder.type(parsed_types[i], arg, packed)
end
end

if tail.size == 0 && packed
tail = head
end

# return the encoded ABI blob
"#{head}#{tail}"
packed ? "#{tail}" : "#{head}#{tail}"
end

# Encodes a custom, packed Application Binary Interface (packed ABI) data.
# It accepts multiple arguments and encodes according to the Solidity specification.
#
# @param types [Array] types to be ABI-encoded.
# @param args [Array] values to be ABI-encoded.
# @return [String] the packed encoded ABI data.
def encode_packed(types, args)
encode(types, args, true)
end

# Decodes Application Binary Interface (ABI) data. It accepts multiple
Expand Down Expand Up @@ -120,6 +145,15 @@ def decode(types, data)
# return the decoded ABI types and data
parsed_types.zip(outputs).map { |(type, out)| Abi::Decoder.type(type, out) }
end

# Since the encoding is ambiguous, there is no decoding function.
#
# @param types [Array] the ABI to be decoded.
# @param data [String] ABI data to be decoded.
# @raise [DecodingError] if you try to decode packed ABI data.
def decode_packed(types, data)
raise DecodingError, "Since the encoding is ambiguous, there is no decoding function."
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/eth/abi/decoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def primitive_type(type, data)
when "address"

# decoded address with 0x-prefix
"0x#{Util.bin_to_hex data[12..-1]}"
Address.new(Util.bin_to_hex data[12..-1]).to_s.downcase
when "string", "bytes"
if type.sub_type.empty?
size = Util.deserialize_big_endian_to_int data[0, 32]
Expand Down
78 changes: 51 additions & 27 deletions lib/eth/abi/encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,28 @@ module Encoder
#
# @param type [Eth::Abi::Type] type to be encoded.
# @param arg [String|Number] value to be encoded.
# @param packed [Boolean] use custom packed encoding.
# @return [String] the encoded type.
# @raise [EncodingError] if value does not match type.
def type(type, arg)
def type(type, arg, packed = false)
if %w(string bytes).include? type.base_type and type.sub_type.empty? and type.dimensions.empty?
raise EncodingError, "Argument must be a String" unless arg.instance_of? String

# encodes strings and bytes
size = type Type.size_type, arg.size
size = type(Type.size_type, arg.size, packed)
padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
return arg if packed
"#{size}#{arg}#{padding}"
elsif type.base_type == "tuple" && type.dimensions.size == 1 && type.dimensions[0] != 0
result = ""
result += struct_offsets(type.nested_sub, arg)
result += arg.map { |x| type(type.nested_sub, x) }.join
result += arg.map { |x| type(type.nested_sub, x, packed) }.join
result
elsif type.dynamic? && arg.is_a?(Array)

# encodes dynamic-sized arrays
head, tail = "", ""
head += type(Type.size_type, arg.size)
head += type(Type.size_type, arg.size, packed) unless packed
nested_sub = type.nested_sub
nested_sub_size = type.nested_sub.size

Expand All @@ -63,25 +65,25 @@ def type(type, arg)
offset += total_bytes_length + 32
end

head += type(Type.size_type, offset)
head += type(Type.size_type, offset, packed)
end
elsif nested_sub.base_type == "tuple" && nested_sub.dynamic?
head += struct_offsets(nested_sub, arg)
end

arg.size.times do |i|
head += type nested_sub, arg[i]
head += type(nested_sub, arg[i], packed)
end
"#{head}#{tail}"
else
if type.dimensions.empty?

# encode a primitive type
primitive_type type, arg
primitive_type type, arg, packed
else

# encode static-size arrays
arg.map { |x| type(type.nested_sub, x) }.join
arg.map { |x| type(type.nested_sub, x, packed) }.join
end
end
end
Expand All @@ -90,30 +92,31 @@ def type(type, arg)
#
# @param type [Eth::Abi::Type] type to be encoded.
# @param arg [String|Number] value to be encoded.
# @param packed [Boolean] use custom packed encoding.
# @return [String] the encoded primitive type.
# @raise [EncodingError] if value does not match type.
# @raise [ValueOutOfBounds] if value is out of bounds for type.
# @raise [EncodingError] if encoding fails for type.
def primitive_type(type, arg)
def primitive_type(type, arg, packed = false)
case type.base_type
when "uint"
uint arg, type
uint arg, type, packed
when "bool"
bool arg
bool arg, packed
when "int"
int arg, type
when "ureal", "ufixed"
int arg, type, packed
when "ureal", "ufixed" # TODO: Q9F
ufixed arg, type
when "real", "fixed"
when "real", "fixed" # TODO: Q9F
fixed arg, type
when "string", "bytes"
bytes arg, type
when "tuple"
bytes arg, type, packed
when "tuple" # TODO: Q9F
tuple arg, type
when "hash"
hash arg, type
hash arg, type, packed
when "address"
address arg
address arg, packed
else
raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
end
Expand All @@ -122,29 +125,39 @@ def primitive_type(type, arg)
private

# Properly encodes unsigned integers.
def uint(arg, type)
def uint(arg, type, packed)
raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::UINT_MAX or arg < Constant::UINT_MIN
real_size = type.sub_type.to_i
i = arg.to_i
raise ValueOutOfBounds, arg unless i >= 0 and i < 2 ** real_size
Util.zpad_int i
if packed
len = real_size / 8
return Util.zpad_int(i, len)
else
return Util.zpad_int(i)
end
end

# Properly encodes signed integers.
def int(arg, type)
def int(arg, type, packed)
raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::INT_MAX or arg < Constant::INT_MIN
real_size = type.sub_type.to_i
i = arg.to_i
raise ValueOutOfBounds, arg unless i >= -2 ** (real_size - 1) and i < 2 ** (real_size - 1)
Util.zpad_int(i % 2 ** 256)
if packed
len = real_size / 8
return Util.zpad_int(i % 2 ** real_size, len)
else
return Util.zpad_int(i % 2 ** 256)
end
end

# Properly encodes booleans.
def bool(arg)
def bool(arg, packed)
raise EncodingError, "Argument is not bool: #{arg}" unless arg.instance_of? TrueClass or arg.instance_of? FalseClass
Util.zpad_int(arg ? 1 : 0)
Util.zpad_int(arg ? 1 : 0, packed ? 1 : 32)
end

# Properly encodes unsigned fixed-point numbers.
Expand All @@ -165,10 +178,13 @@ def fixed(arg, type)
end

# Properly encodes byte-strings.
def bytes(arg, type)
def bytes(arg, type, packed)
raise EncodingError, "Expecting String: #{arg}" unless arg.instance_of? String
arg = handle_hex_string arg, type

# no padding or size handling for packed encoding
return arg if packed

if type.sub_type.empty?
size = Util.zpad_int arg.size
padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
Expand Down Expand Up @@ -235,47 +251,55 @@ def struct_offsets(type, arg)
end

# Properly encodes hash-strings.
def hash(arg, type)
def hash(arg, type, packed)
size = type.sub_type.to_i
raise EncodingError, "Argument too long: #{arg}" unless size > 0 and size <= 32
if arg.is_a? Integer

# hash from integer
return Util.int_to_big_endian arg if packed
Util.zpad_int arg
elsif arg.size == size

# hash from encoded hash
return arg if packed
Util.zpad arg, 32
elsif arg.size == size * 2

# hash from hexadecimal hash
return Util.hex_to_bin arg if packed
Util.zpad_hex arg
else
raise EncodingError, "Could not parse hash: #{arg}"
end
end

# Properly encodes addresses.
def address(arg)
def address(arg, packed)
if arg.is_a? Address

# from checksummed address with 0x prefix
return arg.to_s[2..-1].downcase if packed
Util.zpad_hex arg.to_s[2..-1]
elsif arg.is_a? Integer

# address from integer
return Util.int_to_big_endian arg if packed
Util.zpad_int arg
elsif arg.size == 20

# address from encoded address
return arg if packed
Util.zpad arg, 32
elsif arg.size == 40

# address from hexadecimal address
return Util.hex_to_bin arg if packed
Util.zpad_hex arg
elsif arg.size == 42 and arg[0, 2] == "0x"

# address from hexadecimal address with 0x prefix
return Util.hex_to_bin arg if packed
Util.zpad_hex arg[2..-1]
else
raise EncodingError, "Could not parse address: #{arg}"
Expand Down
Loading
Loading