ABI Encoder extracted from nim-nitro module

This commit is contained in:
Mark Spanbroek 2021-11-25 09:33:32 +01:00
commit 8184529abd
11 changed files with 373 additions and 0 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*]
indent_style = space
insert_final_newline = true
indent_size = 2
trim_trailing_whitespace = true

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!*/
!*.*

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
nim 1.6.0

45
Readme.md Normal file
View File

@ -0,0 +1,45 @@
Contract ABI
============
Implements encoding of parameters according to the Ethereum
[Contract ABI Specification][1].
Installation
------------
Use the [Nimble][2] package manager to add `contractabi` to an existing project.
Add the following to its .nimble file:
```nim
requires "https://github.com/status-im/nim-contract-abi >= 0.1.0 & < 0.2.0"
```
Usage
-----
```nim
import contractabi
import stint
# encode unsigned integers, booleans, enums
AbiEncoder.encode(42'u8)
# encode uint256
AbiEncoder.encode(42.u256)
# encode byte arrays and sequences
AbiEncoder.encode([1'u8, 2'u8, 3'u8])
AbiEncoder.encode(@[1'u8, 2'u8, 3'u8])
# encode tuples
var encoder = AbiEncoder.init()
encoder.startTuple()
encoder.write(42'u8)
encoder.write(@[1'u8, 2'u8, 3'u8])
encoder.write(true)
encoder.finishTuple()
encoder.finish()
```
[1]: https://docs.soliditylang.org/en/latest/abi-spec.html
[2]: https://github.com/nim-lang/nimble

3
contractabi.nim Normal file
View File

@ -0,0 +1,3 @@
import contractabi/encoding
export encoding

7
contractabi.nimble Normal file
View File

@ -0,0 +1,7 @@
version = "0.1.0"
author = "Contract ABI Authors"
description = "ABI Encoding for Ethereum contracts"
license = "MIT"
requires "stint"
requires "upraises >= 0.1.0 & < 0.2.0"

124
contractabi/encoding.nim Normal file
View File

@ -0,0 +1,124 @@
import pkg/stint
import pkg/upraises
push: {.upraises:[].}
type
AbiEncoder* = object
stack: seq[Tuple]
Tuple = object
bytes: seq[byte]
postponed: seq[Split]
dynamic: bool
Split = object
head: Slice[int]
tail: seq[byte]
func write*[T](encoder: var AbiEncoder, value: T)
func encode*[T](_: type AbiEncoder, value: T): seq[byte]
func init*(_: type AbiEncoder): AbiEncoder =
AbiEncoder(stack: @[Tuple()])
func append(tupl: var Tuple, bytes: openArray[byte]) =
tupl.bytes.add(bytes)
func postpone(tupl: var Tuple, bytes: seq[byte]) =
var split: Split
split.head.a = tupl.bytes.len
tupl.append(AbiEncoder.encode(0'u64))
split.head.b = tupl.bytes.high
split.tail = bytes
tupl.postponed.add(split)
func finish(tupl: Tuple): seq[byte] =
var bytes = tupl.bytes
for split in tupl.postponed:
let offset = bytes.len
bytes[split.head] = AbiEncoder.encode(offset.uint64)
bytes.add(split.tail)
bytes
func append(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.stack[^1].append(bytes)
func postpone(encoder: var AbiEncoder, bytes: seq[byte]) =
if encoder.stack.len > 1:
encoder.stack[^1].postpone(bytes)
else:
encoder.stack[0].append(bytes)
func setDynamic(encoder: var AbiEncoder) =
encoder.stack[^1].dynamic = true
func startTuple*(encoder: var AbiEncoder) =
encoder.stack.add(Tuple())
func encode(encoder: var AbiEncoder, tupl: Tuple) =
if tupl.dynamic:
encoder.postpone(tupl.finish())
encoder.setDynamic()
else:
encoder.append(tupl.finish())
func finishTuple*(encoder: var AbiEncoder) =
encoder.encode(encoder.stack.pop())
func pad(encoder: var AbiEncoder, len: int) =
let padlen = (32 - len mod 32) mod 32
for _ in 0..<padlen:
encoder.append([0'u8])
func padleft(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.pad(bytes.len)
encoder.append(bytes)
func padright(encoder: var AbiEncoder, bytes: openArray[byte]) =
encoder.append(bytes)
encoder.pad(bytes.len)
func encode(encoder: var AbiEncoder, value: SomeUnsignedInt | StUint) =
encoder.padleft(value.toBytesBE)
func encode(encoder: var AbiEncoder, value: bool) =
encoder.encode(cast[uint8](value))
func encode(encoder: var AbiEncoder, value: enum) =
encoder.encode(uint64(ord(value)))
func encode[I](encoder: var AbiEncoder, bytes: array[I, byte]) =
encoder.padright(bytes)
func encode(encoder: var AbiEncoder, bytes: seq[byte]) =
encoder.encode(bytes.len.uint64)
encoder.padright(bytes)
encoder.setDynamic()
func encode[I, T](encoder: var AbiEncoder, value: array[I, T]) =
encoder.startTuple()
for element in value:
encoder.write(element)
encoder.finishTuple()
func encode[T](encoder: var AbiEncoder, value: seq[T]) =
encoder.encode(value.len.uint64)
encoder.startTuple()
for element in value:
encoder.write(element)
encoder.finishTuple()
encoder.setDynamic()
func write*[T](encoder: var AbiEncoder, value: T) =
var writer = AbiEncoder.init()
writer.encode(value)
encoder.encode(writer.stack[0])
func finish*(encoder: var AbiEncoder): seq[byte] =
doAssert encoder.stack.len == 1, "not all tuples were finished"
doAssert encoder.stack[0].bytes.len mod 32 == 0, "encoding invariant broken"
encoder.stack[0].bytes
func encode*[T](_: type AbiEncoder, value: T): seq[byte] =
var encoder = AbiEncoder.init()
encoder.write(value)
encoder.finish()

1
nim.cfg Normal file
View File

@ -0,0 +1 @@
--warning[ObservableStores]:"off"

25
tests/examples.nim Normal file
View File

@ -0,0 +1,25 @@
import std/random
import std/sequtils
import pkg/stint
randomize()
proc example*(_: type bool): bool =
rand(0'u8..1'u8) == 1
proc example*[T: SomeInteger](_: type T): T =
rand(T)
proc example*[I: static int, T](_: type array[I, T]): array[I, T] =
for i in 0..<I:
result[i] = T.example
proc example*[T](_: type seq[T], len = 0..5): seq[T] =
let chosenlen = rand(len)
newSeqWith(chosenlen, T.example)
proc example*(_: type UInt256): UInt256 =
UInt256.fromBytes(array[32, byte].example)
proc example*(_: type UInt128): UInt128 =
UInt128.fromBytes(array[16, byte].example)

1
tests/nim.cfg Normal file
View File

@ -0,0 +1 @@
--path:".."

158
tests/testEncoding.nim Normal file
View File

@ -0,0 +1,158 @@
import std/unittest
import pkg/stint
import contractabi
import ./examples
suite "ABI encoding":
proc zeroes(amount: int): seq[byte] =
newSeq[byte](amount)
test "encodes uint8":
check AbiEncoder.encode(42'u8) == 31.zeroes & 42'u8
test "encodes booleans":
check AbiEncoder.encode(false) == 31.zeroes & 0'u8
check AbiEncoder.encode(true) == 31.zeroes & 1'u8
test "encodes uint16, 32, 64":
check AbiEncoder.encode(0xABCD'u16) ==
30.zeroes & 0xAB'u8 & 0xCD'u8
check AbiEncoder.encode(0x11223344'u32) ==
28.zeroes & 0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8
check AbiEncoder.encode(0x1122334455667788'u64) ==
24.zeroes &
0x11'u8 & 0x22'u8 & 0x33'u8 & 0x44'u8 &
0x55'u8 & 0x66'u8 & 0x77'u8 & 0x88'u8
test "encodes ranges":
type SomeRange = range[0x0000'u16..0xAAAA'u16]
check AbiEncoder.encode(SomeRange(0x1122)) == 30.zeroes & 0x11'u8 & 0x22'u8
test "encodes enums":
type SomeEnum = enum
one = 1
two = 2
check AbiEncoder.encode(one) == 31.zeroes & 1'u8
check AbiEncoder.encode(two) == 31.zeroes & 2'u8
test "encodes stints":
let uint256 = UInt256.example
check AbiEncoder.encode(uint256) == @(uint256.toBytesBE)
let uint128 = UInt128.example
check AbiEncoder.encode(uint128) == 16.zeroes & @(uint128.toBytesBE)
test "encodes byte arrays":
let bytes3 = [1'u8, 2'u8, 3'u8]
check AbiEncoder.encode(bytes3) == @bytes3 & 29.zeroes
let bytes32 = array[32, byte].example
check AbiEncoder.encode(bytes32) == @bytes32
let bytes33 = array[33, byte].example
check AbiEncoder.encode(bytes33) == @bytes33 & 31.zeroes
test "encodes byte sequences":
let bytes3 = @[1'u8, 2'u8, 3'u8]
let bytes3len = AbiEncoder.encode(bytes3.len.uint64)
check AbiEncoder.encode(bytes3) == bytes3len & bytes3 & 29.zeroes
let bytes32 = @(array[32, byte].example)
let bytes32len = AbiEncoder.encode(bytes32.len.uint64)
check AbiEncoder.encode(bytes32) == bytes32len & bytes32
let bytes33 = @(array[33, byte].example)
let bytes33len = AbiEncoder.encode(bytes33.len.uint64)
check AbiEncoder.encode(bytes33) == bytes33len & bytes33 & 31.zeroes
test "encodes tuples":
let a = true
let b = @[1'u8, 2'u8, 3'u8]
let c = 0xAABBCCDD'u32
let d = @[4'u8, 5'u8, 6'u8]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.write(b)
encoder.write(c)
encoder.write(d)
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(a) &
AbiEncoder.encode(4 * 32'u8) & # offset in tuple
AbiEncoder.encode(c) &
AbiEncoder.encode(6 * 32'u8) & # offset in tuple
AbiEncoder.encode(b) &
AbiEncoder.encode(d)
test "encodes nested tuples":
let a = true
let b = @[1'u8, 2'u8, 3'u8]
let c = 0xAABBCCDD'u32
let d = @[4'u8, 5'u8, 6'u8]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.write(b)
encoder.startTuple()
encoder.write(c)
encoder.write(d)
encoder.finishTuple()
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(a) &
AbiEncoder.encode(3 * 32'u8) & # offset of b in outer tuple
AbiEncoder.encode(5 * 32'u8) & # offset of inner tuple in outer tuple
AbiEncoder.encode(b) &
AbiEncoder.encode(c) &
AbiEncoder.encode(2 * 32'u8) & # offset of d in inner tuple
AbiEncoder.encode(d)
test "encodes arrays":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected= AbiEncoder.init()
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check AbiEncoder.encode([element1, element2]) == expected.finish()
test "encodes sequences":
let element1 = seq[byte].example
let element2 = seq[byte].example
var expected= AbiEncoder.init()
expected.write(2'u8)
expected.startTuple()
expected.write(element1)
expected.write(element2)
expected.finishTuple()
check AbiEncoder.encode(@[element1, element2]) == expected.finish()
test "encodes sequence as dynamic element":
let s = @[42.u256, 43.u256]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(s)
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(32'u8) & # offset in tuple
AbiEncoder.encode(s)
test "encodes array of static elements as static element":
let a = [[42'u8], [43'u8]]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.finishTuple()
check encoder.finish() == AbiEncoder.encode(a)
test "encodes array of dynamic elements as dynamic element":
let a = [@[42'u8], @[43'u8]]
var encoder= AbiEncoder.init()
encoder.startTuple()
encoder.write(a)
encoder.finishTuple()
check encoder.finish() ==
AbiEncoder.encode(32'u8) & # offset in tuple
AbiEncoder.encode(a)
# https://medium.com/b2expand/abi-encoding-explanation-4f470927092d
# https://docs.soliditylang.org/en/v0.8.1/abi-spec.html#formal-specification-of-the-encoding