nim-rlp version 1.0

This commit is contained in:
Zahary Karadjov 2018-01-25 21:22:13 +02:00
parent 2bfc9e6270
commit 78ec433770
20 changed files with 1327 additions and 1 deletions

5
.gitignore vendored
View File

@ -1 +1,6 @@
nimcache/
# ignore all executable files
*
!*.*
!*/

29
.travis.yml Normal file
View File

@ -0,0 +1,29 @@
language: c
env:
- BRANCH=devel
# - BRANCH=master # At the moment nim-rlp supports only the devel branch of Nim
compiler:
- gcc
- clang
before_install:
# Install nim and nimble
- |
if [ ! -x nim-$BRANCH/bin/nim ]; then
git clone -b $BRANCH --depth 1 git://github.com/nim-lang/nim nim-$BRANCH/
cd nim-$BRANCH
sh ci/build.sh
./koch tools -d:release
else
cd nim-$BRANCH
git fetch origin
if ! git merge FETCH_HEAD | grep "Already up-to-date"; then
bin/nim c koch
./koch boot -d:release
./koch tools -d:release
fi
fi
export PATH=$PWD/bin:$PATH
cd ..
script:
- nimble test

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2018 Status Research & Development GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

136
README.md Normal file
View File

@ -0,0 +1,136 @@
nim-rlp
=======
[![Build Status](https://travis-ci.org/status-im/nim-rlp.svg?branch=master)](https://travis-ci.org/status-im/nim-rlp)
A Nim implementation of the Recursive Length Prefix encoding (RLP) as specified
in the Ethereum's [Yellow Papper](https://ethereum.github.io/yellowpaper/paper.pdf)
and [Wiki](https://github.com/ethereum/wiki/wiki/RLP).
## Reading RLP data
The `Rlp` type provided by this library represents a cursor over a RLP-encoded
byte stream. Before instantiating such a cursor, you must convert your
input data to a `BytesRange` object, which represents an immutable and
thus cheap-to-copy sub-range view over an underlying `seq[byte]` instance:
``` nim
proc initBytesRange*(s: var seq[byte], ibegin = 0, iend = -1): BytesRange
proc rlpFromBytes*(data: BytesRange): Rlp
```
### Streaming API
Once created, the `Rlp` object will offer procs such as `isList`, `isBlob`,
`getType`, `listLen`, `blobLen` to determine the type of the value under
the cursor. The contents of blobs can be extracted with procs such as
`toString`, `toBytes` and `toInt` without advancing the cursor.
Lists can be traversed with the standard `items` iterator, which will advance
the cursor to each sub-item position and yield the `Rlp` object at that point.
As an alternative, `listElem` can return a new `Rlp` object adjusted to a
particular sub-item position without advancing the original cursor.
Keep in mind that copying `Rlp` objects is cheap and you can create as many
cursors pointing to different positions in the RLP stream as necessary.
`skipElem` will advance the cursor to the next position in the current list.
`hasData` will indicate that there are no more bytes in the stream that can
be consumed.
Another way to extract data from the stream is through the universal `read`
proc that accepts a type as a parameter. You can pass any supported type
such as `string`, `int`, `seq[T]`, etc, including composite user-defined
types (see [Object Serialization](#object-serialization)). The cursor
will be advanced just past the end of the consumed object.
The `toXX` and `read` family of procs may raise a `BadCastError` in case
of type mismatch with the stream contents under the cursor. A corrupted
RLP stream or an attemp to read past the stream end will be signaled
with the `MalformedRlpError` exception. If the RLP stream includes data
that cannot be processed on the current platform (e.g. an integer value
that is too large), the library will raise an `UnsupportedRlpError` exception.
### DOM API
Calling `Rlp.toNodes` at any position within the stream will return a tree
of `RlpNode` objects representing the collection of values begging at that
position:
``` nim
type
RlpNodeType* = enum
rlpBlob
rlpList
RlpNode* = object
case kind*: RlpNodeType
of rlpBlob:
bytes*: BytesRange
of rlpList:
elems*: seq[RlpNode]
```
As a short-cut, you can also call `decode` directly on a byte sequence to
avoid creating a `Rlp` object when obtaining the nodes.
For debugging purposes, you can also create a human readable representation
of the Rlp nodes by calling the `inspect` proc:
``` nim
proc inspect*(self: Rlp, indent = 0): string
```
## Creating RLP data
The `RlpWriter` type can be used to encode RLP data. Instances are created
with the `initRlpWriter` proc. This should be followed by one or more calls
to `append` which is overloaded to accept arbitrary values. Finally, you can
call `finish` to obtain the final `BytesRange`.
If the end result should by a RLP list of particular length, you can replace
the initial call to `initRlpWriter` with `initRlpList(n)`. Calling `finish`
before writing a sufficient number of elements will then result in a
`PrematureFinalizationError`.
As an alternative short-cut, you can also call `encode` on an arbitrary value
(including sequences and user-defined types) to execute all of the steps at
once and directly obtain the final RLP bytes. `encodeList(varargs)` is another
short-cut for creating RLP lists.
## Object serialization
As previously explained, generic procs such as `read`, `append`, `encode` and
`decode` can be used with arbitrary used-defined object types. By default, the
library will serialize all of the fields of the object using the `fields`
iterator, but you can modify the order of serialization or include only a
subset of the fields by using the `rlpFields` macro:
``` nim
macro rlpFields*(T: typedesc, fields: varargs[untyped])
## example usage:
type
Transaction = object
amount: int
time: DateTime
sender: string
receiver: string
rlpFields Transaction,
sender, receiver, amount
...
var t1 = rlp.read(Transaction)
var bytes = encode(t1)
var t2 = bytes.decode(Transaction)
```
## Contributing / Testing
To test the correctness of any modifications to the library, please execute
`nimble test` at the root of the repo.
## License
This library is licensed under the Apache 2.0 license.

347
rlp.nim Normal file
View File

@ -0,0 +1,347 @@
## This module implements RLP encoding and decoding as
## defined in Appendix B of the Ethereum Yellow Paper:
## https://ethereum.github.io/yellowpaper/paper.pdf
import
strutils, parseutils,
rlp/[types, writer, object_serialization],
rlp/priv/defs
export
types, writer, object_serialization
type
Rlp* = object
bytes: BytesRange
position: int
RlpNodeType* = enum
rlpBlob
rlpList
RlpNode* = object
case kind*: RlpNodeType
of rlpBlob:
bytes*: BytesRange
of rlpList:
elems*: seq[RlpNode]
MalformedRlpError* = object of Exception
UnsupportedRlpError* = object of Exception
BadCastError* = object of Exception
proc rlpFromBytes*(data: BytesRange): Rlp =
result.bytes = data
result.position = 0
let
zeroBytesRlp* = rlpFromBytes(zeroBytesRange)
proc rlpFromHex*(input: string): Rlp =
if input.len mod 2 != 0:
raise newException(BadCastError,
"The input string len should be even (assuming two characters per byte)")
let totalBytes = input.len div 2
var backingStore = newSeq[byte](totalBytes)
result.bytes = initBytesRange(backingStore)
for i in 0 ..< totalBytes:
var nextByte: int
if parseHex(input, nextByte, i*2, 2) == 2:
result.bytes[i] = byte(nextByte)
else:
raise newException(BadCastError,
"The input string contains invalid characters")
{.this: self.}
proc hasData*(self: Rlp): bool =
position < bytes.len
template rawData*(self: Rlp): BytesRange =
self.bytes
proc isBlob*(self: Rlp): bool =
hasData() and bytes[position] < LIST_START_MARKER
proc isEmpty*(self: Rlp): bool =
### Contains a blob or a list of zero length
hasData() and (bytes[position] == BLOB_START_MARKER or
bytes[position] == LIST_START_MARKER)
proc isList*(self: Rlp): bool =
hasData() and bytes[position] >= LIST_START_MARKER
template eosError =
raise newException(MalformedRlpError, "Read past the end of the RLP stream")
template requireData {.dirty.} =
if not hasData():
raise newException(MalformedRlpError, "Illegal operation over an empty RLP stream")
proc getType*(self: Rlp): RlpNodeType =
requireData()
return if isBlob(): rlpBlob else: rlpList
proc lengthBytesCount(self: Rlp): int =
var marker = bytes[position]
if isBlob() and marker > LEN_PREFIXED_BLOB_MARKER:
return int(marker - LEN_PREFIXED_BLOB_MARKER)
if isList() and marker > LEN_PREFIXED_LIST_MARKER:
return int(marker - LEN_PREFIXED_LIST_MARKER)
return 0
proc isSingleByte(self: Rlp): bool =
hasData() and bytes[position] < BLOB_START_MARKER
proc payloadOffset(self: Rlp): int =
if isSingleByte(): 0 else: 1 + lengthBytesCount()
template readAheadCheck(numberOfBytes) =
if position + numberOfBytes >= bytes.len: eosError()
template nonCanonicalNumberError =
raise newException(MalformedRlpError, "Small number encoded in a non-canonical way")
proc payloadBytesCount(self: Rlp): int =
if not hasData():
return 0
var marker = bytes[position]
if marker < BLOB_START_MARKER:
return 1
if marker <= LEN_PREFIXED_BLOB_MARKER:
result = int(marker - BLOB_START_MARKER)
readAheadCheck(result)
if result == 1:
if bytes[position + 1] < BLOB_START_MARKER:
nonCanonicalNumberError()
return
template readInt(startMarker, lenPrefixMarker) =
var
lengthBytes = int(marker - lenPrefixMarker)
remainingBytes = self.bytes.len - self.position
if remainingBytes <= lengthBytes:
eosError()
if remainingBytes > 1 and self.bytes[self.position + 1] == 0:
raise newException(MalformedRlpError, "Number encoded with a leading zero")
if lengthBytes > sizeof(result):
raise newException(UnsupportedRlpError, "Message too large to fit in memory")
for i in 1 .. lengthBytes:
result = (result shl 8) or int(self.bytes[self.position + i])
# must be greater than the short-list size list
if result < THRESHOLD_LIST_LEN:
nonCanonicalNumberError()
if marker < LIST_START_MARKER:
readInt(BLOB_START_MARKER, LEN_PREFIXED_BLOB_MARKER)
elif marker <= LEN_PREFIXED_LIST_MARKER:
result = int(marker - LIST_START_MARKER)
else:
readInt(LIST_START_MARKER, LEN_PREFIXED_LIST_MARKER)
readAheadCheck(result)
proc blobLen*(self: Rlp): int =
if isBlob(): payloadBytesCount() else: 0
proc isInt*(self: Rlp): bool =
if not hasData():
return false
var marker = bytes[position]
if marker < BLOB_START_MARKER:
return marker != 0
if marker == BLOB_START_MARKER:
return true
if marker <= LEN_PREFIXED_BLOB_MARKER:
return bytes[position + 1] != 0
if marker < LIST_START_MARKER:
let offset = position + int(marker + 1 - LEN_PREFIXED_BLOB_MARKER)
if offset >= bytes.len: eosError()
return bytes[offset] != 0
return false
template maxBytes*(o: typedesc[Ordinal]): int = sizeof(o)
proc toInt*(self: Rlp, IntType: typedesc): IntType =
# XXX: self insertions are not working in generic procs
# https://github.com/nim-lang/Nim/issues/5053
if self.isList() or not self.hasData():
raise newException(BadCastError, "")
let
payloadStart = self.payloadOffset()
payloadSize = self.payloadBytesCount()
if payloadSize > maxBytes(IntType):
raise newException(BadCastError, "")
for i in payloadStart ..< (payloadStart + payloadSize):
result = (result shl 8) or int(self.bytes[self.position + i])
proc toString*(self: Rlp): string =
if not isBlob():
raise newException(BadCastError, "")
let
payloadOffset = payloadOffset()
payloadLen = payloadBytesCount()
remainingBytes = bytes.len - position - payloadOffset
if payloadLen > remainingBytes:
eosError()
result = newString(payloadLen)
for i in 0 ..< payloadLen:
# XXX: switch to copyMem here
result[i] = char(bytes[position + payloadOffset + i])
proc toBytes*(self: Rlp): BytesRange =
if not isBlob():
raise newException(BadCastError, "")
let
payloadOffset = payloadOffset()
payloadLen = payloadBytesCount()
ibegin = position + payloadOffset
iend = ibegin + payloadLen
result = bytes.slice(ibegin, iend)
proc currentElemEnd(self: Rlp): int =
result = position
if not hasData():
return
if isSingleByte():
result += 1
elif isBlob() or isList():
result += payloadOffset() + payloadBytesCount()
proc skipElem*(rlp: var Rlp) =
rlp.position = rlp.currentElemEnd
iterator items*(self: var Rlp): var Rlp =
assert isList()
var
payloadOffset = payloadOffset()
payloadEnd = position + payloadOffset + payloadBytesCount()
if payloadEnd > bytes.iend:
raise newException(MalformedRlpError, "List length extends past the end of the stream")
position += payloadOffset
while position < payloadEnd:
let elemEnd = currentElemEnd()
yield self
position = elemEnd
proc listElem*(self: Rlp, i: int): Rlp =
let payload = bytes.slice payloadOffset()
result = rlpFromBytes payload
var pos = 0
while pos < i and result.hasData:
result.position = result.currentElemEnd()
inc pos
proc listLen*(self: Rlp): int =
if not isList():
return 0
var rlp = self
for elem in rlp:
inc result
proc read*(rlp: var Rlp, T: type string): string =
result = rlp.toString
rlp.skipElem
proc read*(rlp: var Rlp, T: type int): int =
result = rlp.toInt(int)
rlp.skipElem
proc read*[E](rlp: var Rlp, T: typedesc[seq[E]]): T =
if not rlp.isList:
raise newException(BadCastError, "The source RLP is not a list.")
result = newSeqOfCap[E](rlp.listLen)
for elem in rlp:
result.add rlp.read(E)
proc read*(rlp: var Rlp, T: typedesc[object|tuple]): T =
mixin enumerateRlpFields, read
template op(field) =
field = rlp.read(type(field))
enumerateRlpFields(result, op)
proc toNodes*(self: var Rlp): RlpNode =
requireData()
if isList():
result.kind = rlpList
newSeq result.elems, 0
for e in self:
result.elems.add e.toNodes
else:
assert isBlob()
result.kind = rlpBlob
result.bytes = toBytes()
position = currentElemEnd()
proc decode*(bytes: openarray[byte]): RlpNode =
var
bytesCopy = @bytes
rlp = rlpFromBytes initBytesRange(bytesCopy)
return rlp.toNodes
template decode*(bytes: BytesRange, T: typedesc): untyped =
var rlp = rlpFromBytes bytes
rlp.read(T)
template decode*(bytes: openarray[byte], T: typedesc): T =
var bytesCopy = @bytes
decode(initBytesRange(bytesCopy), T)
proc inspectAux(self: var Rlp, depth: int, output: var string) =
if not hasData():
return
template indent =
for i in 0..<depth:
output.add " "
indent()
if self.isSingleByte:
output.add "byte "
output.add $bytes[position]
elif self.isBlob:
output.add '"'
output.add self.toString
output.add '"'
else:
output.add "{\n"
for subitem in self:
inspectAux(subitem, depth + 1, output)
output.add "\n"
indent()
output.add "}"
proc inspect*(self: Rlp, indent = 0): string =
var rlpCopy = self
result = newStringOfCap(bytes.len)
inspectAux(rlpCopy, indent, result)

20
rlp.nimble Normal file
View File

@ -0,0 +1,20 @@
mode = ScriptMode.Verbose
packageName = "rlp"
version = "1.0.0"
author = "Status Research & Development GmbH"
description = "RLP serialization library for Nim"
license = "Apache2"
skipDirs = @["tests"]
requires "nim >= 0.17.0"
proc configForTests() =
--hints: off
--debuginfo
--path: "."
--run
task test, "run CPU tests":
configForTests()
setCommand "c", "tests/all.nim"

View File

@ -0,0 +1,18 @@
import macros
template enumerateRlpFields*[T](x: T, op: untyped) =
for f in fields(x): op(f)
macro rlpFields*(T: typedesc, fields: varargs[untyped]): untyped =
var body = newStmtList()
let
ins = genSym(nskParam, "instance")
op = genSym(nskParam, "op")
for field in fields:
body.add quote do: `op`(`ins`.`field`)
result = quote do:
template enumerateRlpFields*(`ins`: `T`, `op`: untyped) {.inject.} =
`body`

14
rlp/priv/defs.nim Normal file
View File

@ -0,0 +1,14 @@
import
../types
const
MAX_LENGTH_BYTES* = 8
BLOB_START_MARKER* = byte(128)
LIST_START_MARKER* = byte(192)
THRESHOLD_LIST_LEN* = 56
LEN_PREFIXED_BLOB_MARKER* = byte(BLOB_START_MARKER + THRESHOLD_LIST_LEN - 1) # 183
LEN_PREFIXED_LIST_MARKER* = byte(LIST_START_MARKER + THRESHOLD_LIST_LEN - 1) # 247

5
rlp/ptr_arith.nim Normal file
View File

@ -0,0 +1,5 @@
proc baseAddr*[T](x: openarray[T]): pointer = cast[pointer](x)
proc shift*(p: pointer, delta: int): pointer =
cast[pointer](cast[int](p) + delta)

67
rlp/types.nim Normal file
View File

@ -0,0 +1,67 @@
type
Bytes* = seq[byte]
BytesRange* = object
bytes*: Bytes
ibegin*, iend*: int
proc initBytesRange*(s: var Bytes, ibegin = 0, iend = -1): BytesRange =
let e = if iend < 0: s.len + iend + 1
else: iend
assert ibegin >= 0 and e <= s.len
shallow(s)
result.bytes = s
result.ibegin = ibegin
result.iend = e
var
zeroBytes*: Bytes = @[]
zeroBytesRange* = initBytesRange(zeroBytes)
proc `[]`*(r: BytesRange, i: int): byte =
r.bytes[r.ibegin + i]
proc `[]`*(r: var BytesRange, i: int): var byte =
r.bytes[r.ibegin + i]
# XXX: change this to a template after fixing
# https://github.com/nim-lang/Nim/issues/7097
proc `[]=`*(r: var BytesRange, i: int, v: byte) =
r.bytes[r.ibegin + i] = v
template len*(r: BytesRange): int =
r.iend - r.ibegin
proc slice*(r: BytesRange, ibegin: int, iend = -1): BytesRange =
result.bytes = r.bytes
result.ibegin = r.ibegin + ibegin
let e = if iend < 0: r.iend + iend + 1
else: r.ibegin + r.iend
assert ibegin >= 0 and e <= result.bytes.len
result.iend = e
iterator items*(r: BytesRange): byte =
for i in r.ibegin ..< r.iend:
yield r.bytes[i]
converter fromSeq*(s: Bytes): BytesRange =
var seqCopy = s
return initBytesRange(seqCopy)
converter fromVarSeq*(s: var Bytes): BytesRange =
return initBytesRange(s)
when false:
import
ptr_arith, keccak_tiny
type
KeccakHash* = Hash[256]
proc toInputRange*(r: BytesRange): keccak_tiny.InputRange =
result[0] = r.bytes.seqBaseAddr.shift(r.ibegin)
result[1] = r.len
proc keccak*(r: BytesRange): KeccakHash = keccak_256(r)

182
rlp/writer.nim Normal file
View File

@ -0,0 +1,182 @@
import
types, ptr_arith, object_serialization, priv/defs, macros
type
RlpWriter* = object
pendingLists: seq[tuple[remainingItems, outBytes: int]]
output: Bytes
PrematureFinalizationError* = object of Exception
proc bytesNeeded(num: int): int =
var n = num
while n != 0:
inc result
n = n shr 8
proc writeBigEndian(outStream: var Bytes, number: int,
lastByteIdx: int, numberOfBytes: int) {.inline.} =
var n = number
for i in countdown(lastByteIdx, lastByteIdx - int(numberOfBytes) + 1):
outStream[i] = byte(n and 0xff)
n = n shr 8
proc writeBigEndian(outStream: var Bytes, number: int,
numberOfBytes: int) {.inline.} =
outStream.setLen(outStream.len + numberOfBytes)
outStream.writeBigEndian(number, outStream.len - 1, numberOfBytes)
proc writeCount(bytes: var Bytes, count: int, baseMarker: byte) =
if count < THRESHOLD_LIST_LEN:
bytes.add(baseMarker + byte(count))
else:
let
origLen = bytes.len
lenPrefixBytes = count.bytesNeeded
bytes.setLen(origLen + int(lenPrefixBytes) + 1)
bytes[origLen] = baseMarker + (THRESHOLD_LIST_LEN - 1) + byte(lenPrefixBytes)
bytes.writeBigEndian(count, bytes.len - 1, lenPrefixBytes)
proc add(outStream: var Bytes, newChunk: BytesRange) =
let prevLen = outStream.len
outStream.setLen(prevLen + newChunk.len)
# XXX: Use copyMem here
for i in 0 ..< newChunk.len:
outStream[prevLen + i] = newChunk[i]
{.this: self.}
{.experimental.}
using
self: var RlpWriter
proc initRlpWriter*: RlpWriter =
newSeq(result.pendingLists, 0)
newSeq(result.output, 0)
proc decRet(n: var int, delta: int): int =
n -= delta
return n
proc maybeClosePendingLists(self) =
while pendingLists.len > 0:
let lastListIdx = pendingLists.len - 1
assert pendingLists[lastListIdx].remainingItems >= 1
if decRet(pendingLists[lastListIdx].remainingItems, 1) == 0:
# A list have been just finished. It was started in `startList`.
let listStartPos = pendingLists[lastListIdx].outBytes
pendingLists.setLen lastListIdx
# How many bytes were written since the start?
let listLen = output.len - listStartPos
# Compute the number of bytes required to write down the list length
let totalPrefixBytes = if listLen < int(THRESHOLD_LIST_LEN): 1
else: int(listLen.bytesNeeded) + 1
# Shift the written data to make room for the prefix length
output.setLen(output.len + totalPrefixBytes)
let outputBaseAddr = output.baseAddr
moveMem(outputBaseAddr.shift(listStartPos + totalPrefixBytes),
outputBaseAddr.shift(listStartPos),
listLen)
# Write out the prefix length
if listLen < THRESHOLD_LIST_LEN:
output[listStartPos] = LIST_START_MARKER + byte(listLen)
else:
let listLenBytes = totalPrefixBytes - 1
output[listStartPos] = LEN_PREFIXED_LIST_MARKER + byte(listLenBytes)
output.writeBigEndian(listLen, listStartPos + listLenBytes, listLenBytes)
else:
# The currently open list is not finished yet. Nothing to do.
return
proc appendRawList(self; bytes: BytesRange) =
output.writeCount(bytes.len, LIST_START_MARKER)
output.add(bytes)
maybeClosePendingLists()
proc startList(self; listSize: int) =
if listSize == 0:
appendRawList zeroBytesRange
else:
pendingLists.add((listSize, output.len))
template appendImpl(self; data, startMarker) =
if data.len == 1 and byte(data[0]) < BLOB_START_MARKER:
output.add byte(data[0])
else:
output.writeCount(data.len, startMarker)
let startPos = output.len
output.setLen(startPos + data.len)
copyMem(output.baseAddr.shift(startPos), data.baseAddr, data.len)
maybeClosePendingLists()
proc append*(self; data: string) =
appendImpl(self, data, BLOB_START_MARKER)
proc append*(self; i: int) =
if i == 0:
output.add BLOB_START_MARKER
elif i < int(BLOB_START_MARKER):
output.add byte(i)
else:
let bytesNeeded = i.bytesNeeded
output.writeCount(bytesNeeded, BLOB_START_MARKER)
output.writeBigEndian(i, bytesNeeded)
maybeClosePendingLists()
proc append*[T](self; list: openarray[T]) =
self.startList list.len
for i in 0 ..< list.len:
self.append list[i]
proc append*(self; data: object|tuple) =
mixin enumerateRlpFields, append
template op(x) = append(self, x)
enumerateRlpFields(data, op)
proc initRlpList*(listSize: int): RlpWriter =
result = initRlpWriter()
startList(result, listSize)
proc finish*(self): BytesRange =
if pendingLists.len > 0:
raise newException(PrematureFinalizationError,
"Insufficient number of elements written to a started list")
result = initBytesRange(output)
proc encode*[T](v: T): BytesRange =
var writer = initRlpWriter()
writer.append(v)
return writer.finish
macro encodeList*(args: varargs[untyped]): BytesRange =
var
listLen = args.len
writer = genSym(nskVar, "rlpWriter")
body = newStmtList()
for arg in args:
body.add quote do:
`writer`.append(`arg`)
result = quote do:
var `writer` = initRlpList(`listLen`)
`body`
`writer`.finish
when false:
# XXX: Currently fails with a malformed AST error on the args.len expression
template encodeList*(args: varargs[untyped]): BytesRange =
var writer = initRlpList(args.len)
for arg in args:
writer.append(arg)
writer.finish

3
tests/all.nim Normal file
View File

@ -0,0 +1,3 @@
import
test_api_usage, test_json_suite, test_object_serialization

View File

@ -0,0 +1,6 @@
{
"listsoflists2": {
"in": "VALID",
"out": "c7c0c1c0c3c0c1c0"
}
}

View File

@ -0,0 +1,46 @@
{
"int32Overflow": {
"in": "INVALID",
"out": "bf0f000000000000021111"
},
"int32Overflow2": {
"in": "INVALID",
"out": "ff0f000000000000021111"
},
"wrongSizeList": {
"in": "INVALID",
"out": "f80180"
},
"wrongSizeList2": {
"in": "INVALID",
"out": "f80100"
},
"incorrectLengthInArray": {
"in": "INVALID",
"out": "b9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df0"
},
"randomRLP": {
"in": "INVALID",
"out": "f861f83eb9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df027b90015002d5ef8325ae4d034df55d4b58d0dfba64d61ddd17be00000b9001a00dae30907045a2f66fa36f2bb8aa9029cbb0b8a7b3b5c435ab331"
},
"bytesShouldBeSingleByte00": {
"in": "INVALID",
"out": "8100"
},
"bytesShouldBeSingleByte01": {
"in": "INVALID",
"out": "8100"
},
"bytesShouldBeSingleByte7F": {
"in": "INVALID",
"out": "817F"
}
}

View File

@ -0,0 +1,67 @@
{
"T1": {
"in": "INVALID",
"out": ""
},
"T2": {
"in": "INVALID",
"out": "00ab"
},
"T3": {
"in": "INVALID",
"out": "0000ff"
},
"T4": {
"in": "VALID",
"out": "83646F67636174"
},
"T5": {
"in": "INVALID",
"out": "83646F"
},
"T6": {
"in": "INVALID",
"out": "c7c0c1c0c3c0c1c0ff"
},
"T7": {
"in": "INVALID",
"out": "c7c0c1c0c3c0c1"
},
"T8": {
"in": "INVALID",
"out": "8102"
},
"T9": {
"in": "INVALID",
"out": "b800"
},
"T10": {
"in": "INVALID",
"out": "b800"
},
"T11": {
"in": "INVALID",
"out": "b90000"
},
"T12": {
"in": "INVALID",
"out": "ba0002ffff"
},
"T13": {
"in": "INVALID",
"out": "8154"
}
}

158
tests/cases/rlptest.json Normal file
View File

@ -0,0 +1,158 @@
{
"emptystring": {
"in": "",
"out": "80"
},
"bytestring00": {
"in": "\u0000",
"out": "00"
},
"bytestring01": {
"in": "\u0001",
"out": "01"
},
"bytestring7F": {
"in": "\u007F",
"out": "7f"
},
"shortstring": {
"in": "dog",
"out": "83646f67"
},
"shortstring2": {
"in": "Lorem ipsum dolor sit amet, consectetur adipisicing eli",
"out": "b74c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c69"
},
"longstring": {
"in": "Lorem ipsum dolor sit amet, consectetur adipisicing elit",
"out": "b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974"
},
"longstring2": {
"in": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat",
"out": "b904004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20437572616269747572206d6175726973206d61676e612c20737573636970697420736564207665686963756c61206e6f6e2c20696163756c697320666175636962757320746f72746f722e2050726f696e20737573636970697420756c74726963696573206d616c6573756164612e204475697320746f72746f7220656c69742c2064696374756d2071756973207472697374697175652065752c20756c7472696365732061742072697375732e204d6f72626920612065737420696d70657264696574206d6920756c6c616d636f7270657220616c6971756574207375736369706974206e6563206c6f72656d2e2041656e65616e2071756973206c656f206d6f6c6c69732c2076756c70757461746520656c6974207661726975732c20636f6e73657175617420656e696d2e204e756c6c6120756c74726963657320747572706973206a7573746f2c20657420706f73756572652075726e6120636f6e7365637465747572206e65632e2050726f696e206e6f6e20636f6e76616c6c6973206d657475732e20446f6e65632074656d706f7220697073756d20696e206d617572697320636f6e67756520736f6c6c696369747564696e2e20566573746962756c756d20616e746520697073756d207072696d697320696e206661756369627573206f726369206c756374757320657420756c74726963657320706f737565726520637562696c69612043757261653b2053757370656e646973736520636f6e76616c6c69732073656d2076656c206d617373612066617563696275732c2065676574206c6163696e6961206c616375732074656d706f722e204e756c6c61207175697320756c747269636965732070757275732e2050726f696e20617563746f722072686f6e637573206e69626820636f6e64696d656e74756d206d6f6c6c69732e20416c697175616d20636f6e73657175617420656e696d206174206d65747573206c75637475732c206120656c656966656e6420707572757320656765737461732e20437572616269747572206174206e696268206d657475732e204e616d20626962656e64756d2c206e6571756520617420617563746f72207472697374697175652c206c6f72656d206c696265726f20616c697175657420617263752c206e6f6e20696e74657264756d2074656c6c7573206c65637475732073697420616d65742065726f732e20437261732072686f6e6375732c206d65747573206163206f726e617265206375727375732c20646f6c6f72206a7573746f20756c747269636573206d657475732c20617420756c6c616d636f7270657220766f6c7574706174"
},
"zero": {
"in": 0,
"out": "80"
},
"smallint": {
"in": 1,
"out": "01"
},
"smallint2": {
"in": 16,
"out": "10"
},
"smallint3": {
"in": 79,
"out": "4f"
},
"smallint4": {
"in": 127,
"out": "7f"
},
"mediumint1": {
"in": 128,
"out": "8180"
},
"mediumint2": {
"in": 1000,
"out": "8203e8"
},
"mediumint3": {
"in": 100000,
"out": "830186a0"
},
"mediumint4": {
"in": "#83729609699884896815286331701780722",
"out": "8f102030405060708090a0b0c0d0e0f2"
},
"mediumint5": {
"in": "#105315505618206987246253880190783558935785933862974822347068935681",
"out": "9c0100020003000400050006000700080009000a000b000c000d000e01"
},
"emptylist": {
"in": [],
"out": "c0"
},
"stringlist": {
"in": [ "dog", "god", "cat" ],
"out": "cc83646f6783676f6483636174"
},
"multilist": {
"in": [ "zw", [ 4 ], 1 ],
"out": "c6827a77c10401"
},
"shortListMax1": {
"in": [ "asdf", "qwer", "zxcv", "asdf","qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"],
"out": "f784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572"
},
"longList1" : {
"in" : [
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"]
],
"out": "f840cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376"
},
"longList2" : {
"in" : [
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"],
["asdf","qwer","zxcv"]
],
"out": "f90200cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376"
},
"listsoflists": {
"in": [ [ [], [] ], [] ],
"out": "c4c2c0c0c0"
},
"listsoflists2": {
"in": [ [], [[]], [ [], [[]] ] ],
"out": "c7c0c1c0c3c0c1c0"
},
"dictTest1" : {
"in" : [
["key1", "val1"],
["key2", "val2"],
["key3", "val3"],
["key4", "val4"]
],
"out" : "ecca846b6579318476616c31ca846b6579328476616c32ca846b6579338476616c33ca846b6579348476616c34"
},
"bigint": {
"in": "#115792089237316195423570985008687907853269984665640564039457584007913129639936",
"out": "a1010000000000000000000000000000000000000000000000000000000000000000"
}
}

91
tests/test_api_usage.nim Normal file
View File

@ -0,0 +1,91 @@
import
unittest, strutils,
rlp, util/json_testing
proc q(s: string): string = "\"" & s & "\""
proc i(s: string): string = s.replace(" ").replace("\n")
proc inspectMatch(r: Rlp, s: string): bool = r.inspect.i == s.i
test "empty bytes are not a proper RLP":
var rlp = rlpFromBytes Bytes(@[])
check:
not rlp.hasData
not rlp.isBlob
not rlp.isList
not rlp.isEmpty
expect Exception:
discard rlp.getType
expect Exception:
for e in rlp:
discard e.getType
test "you cannot finish a list without appending enough elements":
var writer = initRlpList(3)
writer.append "foo"
writer.append "bar"
expect PrematureFinalizationError:
let result = writer.finish
proc withNewLines(x: string): string = x & "\n"
test "encode and decode lists":
var writer = initRlpList(3)
writer.append "foo"
writer.append ["bar", "baz"]
writer.append [30, 40, 50]
var
bytes = writer.finish
rlp = rlpFromBytes bytes
check:
bytes.hexRepr == "d183666f6fc8836261728362617ac31e2832"
rlp.inspectMatch """
{
"foo"
{
"bar"
"baz"
}
{
byte 30
byte 40
byte 50
}
}
"""
bytes = encodeList(6000,
"Lorem ipsum dolor sit amet",
"Donec ligula tortor, egestas eu est vitae")
rlp = rlpFromBytes bytes
check:
rlp.listLen == 3
rlp.listElem(0).toInt(int) == 6000
rlp.listElem(1).toString == "Lorem ipsum dolor sit amet"
rlp.listElem(2).toString == "Donec ligula tortor, egestas eu est vitae"
test "encoding length":
let listBytes = encode([1,2,3,4,5])
let listRlp = rlpFromBytes listBytes
check listRlp.listLen == 5
let emptyListBytes = encode ""
check emptyListBytes.len == 1
let emptyListRlp = rlpFromBytes emptyListBytes
check emptyListRlp.blobLen == 0
test "basic decoding":
var rlp = rlpFromHex("856d6f6f7365")
check rlp.inspect == q"moose"
test "malformed/truncated RLP":
var rlp = rlpFromHex("b8056d6f6f7365")
expect MalformedRlpError:
discard rlp.inspect

View File

@ -0,0 +1,8 @@
import
os, strutils,
util/json_testing
for file in walkDirRec("tests/cases"):
if file.endsWith("json"):
runTests(file)

View File

@ -0,0 +1,50 @@
import
unittest, times, rlp, util/json_testing
type
Transaction = object
amount: int
time: DateTime
sender: string
receiver: string
Foo = object
x: int
y: string
z: seq[int]
Bar = object
b: string
f: Foo
rlpFields Foo,
x, y, z
rlpFields Transaction,
sender, receiver, amount
proc default(T: typedesc): T = discard
test "encoding and decoding an object":
var originalBar = Bar(b: "abracadabra",
f: Foo(x: 5, y: "hocus pocus", z: @[100, 200, 300]))
var bytes = encode(originalBar)
var r = rlpFromBytes(bytes)
var restoredBar = r.read(Bar)
check:
originalBar == restoredBar
var t1 = Transaction(time: now(), amount: 1000, sender: "Alice", receiver: "Bob")
bytes = encode(t1)
var t2 = bytes.decode(Transaction)
check:
bytes.hexRepr == "85416c69636583426f628203e8" # verifies that Alice comes first
t2.time == default(DateTime)
t2.sender == "Alice"
t2.receiver == "Bob"
t2.amount == 1000

74
util/json_testing.nim Normal file
View File

@ -0,0 +1,74 @@
import
json, strutils, rlp
proc append(output: var RlpWriter, js: JsonNode) =
case js.kind
of JNull, JFloat, JObject:
raise newException(ValueError, "Unsupported JSON value type " & $js.kind)
of JBool:
output.append js.bval.int
of JInt:
output.append int(js.num)
of JString:
output.append js.str
of JArray:
output.append js.elems
proc hexRepr*(bytes: BytesRange): string =
result = newStringOfCap(bytes.len * 2)
for byte in bytes:
result.add(toHex(int(byte), 2).toLowerAscii)
proc `==`(lhs: JsonNode, rhs: string): bool =
lhs.kind == JString and lhs.str == rhs
proc runTests*(filename: string) =
let js = json.parseFile(filename)
for testname, testdata in js:
template testStatus(status: string) =
echo status, " ", filename, " :: ", testname
let
input = testdata{"in"}
output = testdata{"out"}
if input.isNil or output.isNil or output.kind != JString:
testStatus "IGNORED"
continue
if input == "VALID":
var rlp = rlpFromHex(output.str)
discard rlp.inspect
elif input == "INVALID":
var success = false
var inspectOutput = ""
try:
var rlp = rlpFromHex(output.str)
inspectOutput = rlp.inspect(1)
discard rlp.getType
while rlp.hasData: discard rlp.toNodes
except MalformedRlpError, ValueError:
success = true
if not success:
testStatus "FAILED"
echo " ACCEPTED MALFORMED BYTES: ", output.str
echo " INTERPRETATION:\n", inspectOutput
continue
else:
if input.kind == JString and input.str[0] == '#':
continue
var outRlp = initRlpWriter()
outRlp.append input
let
actual = outRlp.finish.hexRepr
expected = output.str
if actual != expected:
testStatus "FAILED"
echo " EXPECTED BYTES: ", expected
echo " ACTUAL BYTES: ", actual
continue
testStatus "OK"