nim-eth/eth/rlp/writer.nim

315 lines
10 KiB
Nim
Raw Normal View History

2019-02-05 12:01:10 +00:00
import
std/options,
results,
stew/[arraybuf, assign2, bitops2, shims/macros],
./priv/defs
2019-02-05 12:01:10 +00:00
export arraybuf
2019-02-05 12:01:10 +00:00
type
RlpWriter* = object
pendingLists: seq[tuple[remainingItems, outBytes: int]]
output: seq[byte]
2019-02-05 12:01:10 +00:00
RlpIntBuf* = ArrayBuf[9, byte]
## Small buffer for holding a single RLP-encoded integer
2019-02-05 12:01:10 +00:00
const
wrapObjsInList* = true
func bytesNeeded(num: SomeUnsignedInt): int =
# Number of non-zero bytes in the big endian encoding
sizeof(num) - (num.leadingZeros() shr 3)
2019-02-05 12:01:10 +00:00
func writeBigEndian(outStream: var auto, number: SomeUnsignedInt,
2019-02-05 12:01:10 +00:00
lastByteIdx: int, numberOfBytes: int) =
var n = number
for i in countdown(lastByteIdx, lastByteIdx - numberOfBytes + 1):
2019-02-05 12:01:10 +00:00
outStream[i] = byte(n and 0xff)
n = n shr 8
func writeBigEndian(outStream: var auto, number: SomeUnsignedInt,
2019-02-05 12:01:10 +00:00
numberOfBytes: int) {.inline.} =
outStream.setLen(outStream.len + numberOfBytes)
outStream.writeBigEndian(number, outStream.len - 1, numberOfBytes)
func writeCount(bytes: var auto, count: int, baseMarker: byte) =
2019-02-05 12:01:10 +00:00
if count < THRESHOLD_LIST_LEN:
bytes.add(baseMarker + byte(count))
else:
let
origLen = bytes.len
lenPrefixBytes = uint64(count).bytesNeeded
2019-02-05 12:01:10 +00:00
2024-10-24 08:25:50 +00:00
bytes.setLen(origLen + lenPrefixBytes + 1)
2019-02-05 12:01:10 +00:00
bytes[origLen] = baseMarker + (THRESHOLD_LIST_LEN - 1) + byte(lenPrefixBytes)
bytes.writeBigEndian(uint64(count), bytes.len - 1, lenPrefixBytes)
2019-02-05 12:01:10 +00:00
func writeInt(outStream: var auto, i: SomeUnsignedInt) =
if i == typeof(i)(0):
outStream.add BLOB_START_MARKER
elif i < typeof(i)(BLOB_START_MARKER):
outStream.add byte(i)
else:
let bytesNeeded = i.bytesNeeded
outStream.writeCount(bytesNeeded, BLOB_START_MARKER)
outStream.writeBigEndian(i, bytesNeeded)
2019-02-05 12:01:10 +00:00
proc initRlpWriter*: RlpWriter =
# Avoid allocations during initial write of small items - since the writer is
# expected to be short-lived, it doesn't hurt to allocate this buffer
result.output = newSeqOfCap[byte](2000)
2019-02-05 12:01:10 +00:00
proc decRet(n: var int, delta: int): int =
n -= delta
n
2019-02-05 12:01:10 +00:00
proc maybeClosePendingLists(self: var RlpWriter) =
while self.pendingLists.len > 0:
let lastListIdx = self.pendingLists.len - 1
doAssert self.pendingLists[lastListIdx].remainingItems >= 1
if decRet(self.pendingLists[lastListIdx].remainingItems, 1) == 0:
2019-02-05 12:01:10 +00:00
# A list have been just finished. It was started in `startList`.
let listStartPos = self.pendingLists[lastListIdx].outBytes
self.pendingLists.setLen lastListIdx
2019-02-05 12:01:10 +00:00
# How many bytes were written since the start?
let listLen = self.output.len - listStartPos
2019-02-05 12:01:10 +00:00
# Compute the number of bytes required to write down the list length
let totalPrefixBytes = if listLen < int(THRESHOLD_LIST_LEN): 1
else: int(uint64(listLen).bytesNeeded) + 1
2019-02-05 12:01:10 +00:00
# Shift the written data to make room for the prefix length
self.output.setLen(self.output.len + totalPrefixBytes)
2019-02-05 12:01:10 +00:00
moveMem(addr self.output[listStartPos + totalPrefixBytes],
unsafeAddr self.output[listStartPos],
2019-02-05 12:01:10 +00:00
listLen)
# Write out the prefix length
if listLen < THRESHOLD_LIST_LEN:
self.output[listStartPos] = LIST_START_MARKER + byte(listLen)
2019-02-05 12:01:10 +00:00
else:
let listLenBytes = totalPrefixBytes - 1
self.output[listStartPos] = LEN_PREFIXED_LIST_MARKER + byte(listLenBytes)
self.output.writeBigEndian(uint64(listLen), listStartPos + listLenBytes, listLenBytes)
2019-02-05 12:01:10 +00:00
else:
# The currently open list is not finished yet. Nothing to do.
return
proc appendRawBytes*(self: var RlpWriter, bytes: openArray[byte]) =
self.output.setLen(self.output.len + bytes.len)
assign(self.output.toOpenArray(
self.output.len - bytes.len, self.output.len - 1), bytes)
self.maybeClosePendingLists()
2019-02-05 12:01:10 +00:00
proc startList*(self: var RlpWriter, listSize: int) =
2019-02-05 12:01:10 +00:00
if listSize == 0:
2024-10-24 08:25:50 +00:00
self.output.writeCount(0, LIST_START_MARKER)
self.appendRawBytes([])
2019-02-05 12:01:10 +00:00
else:
self.pendingLists.add((listSize, self.output.len))
2019-02-05 12:01:10 +00:00
proc appendBlob(self: var RlpWriter, data: openArray[byte], startMarker: byte) =
2019-02-05 12:01:10 +00:00
if data.len == 1 and byte(data[0]) < BLOB_START_MARKER:
self.output.add byte(data[0])
self.maybeClosePendingLists()
2019-02-05 12:01:10 +00:00
else:
self.output.writeCount(data.len, startMarker)
self.appendRawBytes(data)
2019-02-05 12:01:10 +00:00
proc appendImpl(self: var RlpWriter, data: string) =
appendBlob(self, data.toOpenArrayByte(0, data.high), BLOB_START_MARKER)
2019-02-05 12:01:10 +00:00
proc appendBlob(self: var RlpWriter, data: openArray[byte]) =
2019-02-05 12:01:10 +00:00
appendBlob(self, data, BLOB_START_MARKER)
proc appendBlob(self: var RlpWriter, data: openArray[char]) =
appendBlob(self, data.toOpenArrayByte(0, data.high), BLOB_START_MARKER)
2019-02-05 12:01:10 +00:00
proc appendInt(self: var RlpWriter, i: SomeUnsignedInt) =
2019-02-05 12:01:10 +00:00
# this is created as a separate proc as an extra precaution against
# any overloading resolution problems when matching the IntLike concept.
self.output.writeInt(i)
2019-02-05 12:01:10 +00:00
self.maybeClosePendingLists()
template appendImpl(self: var RlpWriter, i: SomeUnsignedInt) =
2019-02-05 12:01:10 +00:00
appendInt(self, i)
template appendImpl(self: var RlpWriter, e: enum) =
2019-02-05 12:01:10 +00:00
appendImpl(self, int(e))
template appendImpl(self: var RlpWriter, b: bool) =
2019-02-05 12:01:10 +00:00
appendImpl(self, int(b))
proc appendImpl[T](self: var RlpWriter, listOrBlob: openArray[T]) =
2019-02-05 12:01:10 +00:00
mixin append
# TODO: This append proc should be overloaded by `openArray[byte]` after
2019-02-05 12:01:10 +00:00
# nim bug #7416 is fixed.
when T is (byte or char):
self.appendBlob(listOrBlob)
else:
self.startList listOrBlob.len
for i in 0 ..< listOrBlob.len:
self.append listOrBlob[i]
proc countOptionalFields(T: type): int {.compileTime.} =
mixin enumerateRlpFields
2024-10-24 08:25:50 +00:00
var dummy: T
2024-10-24 08:25:50 +00:00
# closure signature matches the one in object_serialization.nim
template op(RT, fN, f) =
2024-10-24 08:25:50 +00:00
when f is Option or f is Opt:
inc result
else: # this will count only optional fields at the end
result = 0
enumerateRlpFields(dummy, op)
proc genPrevFields(obj: NimNode, fd: openArray[FieldDescription], hi, lo: int): NimNode =
result = newStmtList()
for i in countdown(hi, lo):
let fieldName = fd[i].name
let msg = fieldName.strVal & " expected"
result.add quote do:
doAssert(`obj`.`fieldName`.isSome, `msg`)
macro genOptionalFieldsValidation(obj: untyped, T: type, num: static[int]): untyped =
let
Tresolved = getType(T)[1]
fd = recordFields(Tresolved.getImpl)
loidx = fd.len-num
result = newStmtList()
for i in countdown(fd.high, loidx):
let fieldName = fd[i].name
let prevFields = genPrevFields(obj, fd, i-1, loidx-1)
result.add quote do:
if `obj`.`fieldName`.isSome:
`prevFields`
# generate something like
when false:
if obj.fee.isNone:
doAssert(obj.withdrawalsRoot.isNone, "withdrawalsRoot needs fee")
doAssert(obj.blobGasUsed.isNone, "blobGasUsed needs fee")
doAssert(obj.excessBlobGas.isNone, "excessBlobGas needs fee")
if obj.withdrawalsRoot.isNone:
doAssert(obj.blobGasUsed.isNone, "blobGasUsed needs withdrawalsRoot")
doAssert(obj.excessBlobGas.isNone, "excessBlobGas needs withdrawalsRoot")
doAssert obj.blobGasUsed.isSome == obj.excessBlobGas.isSome,
"blobGasUsed and excessBlobGas must both be present or absent"
proc countFieldsRuntime(obj: object|tuple): int =
2024-10-24 08:25:50 +00:00
mixin enumerateRlpFields
var numOptionals: int = 0
2024-10-24 08:25:50 +00:00
template op(RT, fN, f) {.used.} =
when f is Option or f is Opt:
if f.isSome: # if optional and non empty
inc numOptionals
2024-10-24 08:25:50 +00:00
else: # if mandatory field
inc result
numOptionals = 0 # count only optionals at the end (after mandatory)
2024-10-24 08:25:50 +00:00
enumerateRlpFields(obj, op)
result += numOptionals
proc appendRecordType*(self: var RlpWriter, obj: object|tuple, wrapInList = wrapObjsInList) =
2019-02-05 12:01:10 +00:00
mixin enumerateRlpFields, append
type ObjType = type obj
const
2024-10-24 08:25:50 +00:00
cof = countOptionalFields(ObjType)
2024-10-24 08:25:50 +00:00
when cof > 0:
# ignoring first optional fields
genOptionalFieldsValidation(obj, ObjType, cof - 1)
2019-02-05 12:01:10 +00:00
if wrapInList:
2024-10-24 08:25:50 +00:00
when cof > 0:
self.startList(obj.countFieldsRuntime)
else:
2024-10-24 08:25:50 +00:00
self.startList(ObjType.rlpFieldsCount)
2019-02-05 12:01:10 +00:00
2024-02-13 12:34:27 +00:00
template op(RecordType, fieldName, field) {.used.} =
when hasCustomPragmaFixed(RecordType, fieldName, rlpCustomSerialization):
2019-02-05 12:01:10 +00:00
append(self, obj, field)
2024-10-24 08:25:50 +00:00
elif (field is Option or field is Opt) and cof > 0:
# this works for optional fields at the end of an object/tuple
# if the optional field is followed by a mandatory field,
# custom serialization for a field or for the parent object
# will be better
if field.isSome:
append(self, field.unsafeGet)
2019-02-05 12:01:10 +00:00
else:
append(self, field)
enumerateRlpFields(obj, op)
proc appendImpl(self: var RlpWriter, data: object) {.inline.} =
self.appendRecordType(data)
2019-02-05 12:01:10 +00:00
proc appendImpl(self: var RlpWriter, data: tuple) {.inline.} =
2019-02-05 12:01:10 +00:00
self.appendRecordType(data)
2022-11-16 16:44:00 +00:00
# We define a single `append` template with a pretty low specificity
2019-02-05 12:01:10 +00:00
# score in order to facilitate easier overloading with user types:
template append*[T](w: var RlpWriter; data: T) =
when data is (enum|bool):
# TODO detect negative enum values at compile time?
appendImpl(w, uint64(data))
2019-02-05 12:01:10 +00:00
else:
appendImpl(w, data)
template append*(w: var RlpWriter; data: SomeSignedInt) =
{.error: "Signed integer encoding is not defined for rlp".}
2019-02-05 12:01:10 +00:00
proc initRlpList*(listSize: int): RlpWriter =
result = initRlpWriter()
startList(result, listSize)
# TODO: This should return a lent value
template finish*(self: RlpWriter): seq[byte] =
doAssert self.pendingLists.len == 0, "Insufficient number of elements written to a started list"
self.output
2019-02-05 12:01:10 +00:00
func clear*(w: var RlpWriter) =
# Prepare writer for reuse
w.pendingLists.setLen(0)
w.output.setLen(0)
proc encode*[T](v: T): seq[byte] =
2019-02-05 12:01:10 +00:00
mixin append
2019-02-05 12:01:10 +00:00
var writer = initRlpWriter()
writer.append(v)
move(writer.finish)
2019-02-05 12:01:10 +00:00
func encodeInt*(i: SomeUnsignedInt): RlpIntBuf =
var buf: RlpIntBuf
buf.writeInt(i)
buf
2019-02-05 12:01:10 +00:00
macro encodeList*(args: varargs[untyped]): seq[byte] =
2019-02-05 12:01:10 +00:00
var
listLen = args.len
writer = genSym(nskVar, "rlpWriter")
body = newStmtList()
append = bindSym("append", brForceOpen)
for arg in args:
body.add quote do:
`append`(`writer`, `arg`)
result = quote do:
var `writer` = initRlpList(`listLen`)
`body`
move(finish(`writer`))