nimbus-eth2/beacon_chain/ssz/codec.nim

255 lines
9.5 KiB
Nim
Raw Normal View History

# beacon_chain
# Copyright (c) 2018-2021 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [Defect].}
{.pragma: raisesssz, raises: [Defect, MalformedSszError, SszSizeMismatchError].}
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
# Coding and decoding of primitive SSZ types - every "simple" type passed to
# and from the SSZ library must have a `fromSssBytes` and `toSszType` overload.
import
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
std/typetraits,
stew/[endians2, objects],
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
../spec/digest, ./types
export
digest, types
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
template raiseIncorrectSize*(T: type) =
const typeName = name(T)
raise newException(MalformedSszError,
"SSZ " & typeName & " input of incorrect size")
2020-01-24 18:35:15 +00:00
template setOutputSize[R, T](a: var array[R, T], length: int) =
if length != a.len:
raiseIncorrectSize a.type
2020-01-24 18:35:15 +00:00
proc setOutputSize(list: var List, length: int) {.raisesssz.} =
if not list.setLen length:
raise newException(MalformedSszError, "SSZ list maximum size exceeded")
2020-01-24 18:35:15 +00:00
# fromSszBytes copies the wire representation to a Nim variable,
# assuming there's enough data in the buffer
2020-10-28 18:35:31 +00:00
func fromSszBytes*(T: type UintN, data: openArray[byte]): T {.raisesssz.} =
## Convert directly to bytes the size of the int. (e.g. ``uint16 = 2 bytes``)
## All integers are serialized as **little endian**.
if data.len != sizeof(result):
raiseIncorrectSize T
2020-01-24 18:35:15 +00:00
2020-03-05 00:29:27 +00:00
T.fromBytesLE(data)
2020-10-28 18:35:31 +00:00
func fromSszBytes*(T: type bool, data: openArray[byte]): T {.raisesssz.} =
# Strict: only allow 0 or 1
if data.len != 1 or byte(data[0]) > byte(1):
2020-01-24 18:35:15 +00:00
raise newException(MalformedSszError, "invalid boolean value")
data[0] == 1
2020-10-28 18:35:31 +00:00
func fromSszBytes*(T: type Eth2Digest, data: openArray[byte]): T {.raisesssz.} =
if data.len != sizeof(result.data):
raiseIncorrectSize T
copyMem(result.data.addr, unsafeAddr data[0], sizeof(result.data))
2020-10-28 18:35:31 +00:00
template fromSszBytes*(T: type BitSeq, bytes: openArray[byte]): auto =
BitSeq @bytes
proc `[]`[T, U, V](s: openArray[T], x: HSlice[U, V]) {.error:
2020-10-28 18:35:31 +00:00
"Please don't use openArray's [] as it allocates a result sequence".}
template checkForForbiddenBits(ResulType: type,
2020-10-28 18:35:31 +00:00
input: openArray[byte],
expectedBits: static int64) =
## This checks if the input contains any bits set above the maximum
## sized allowed. We only need to check the last byte to verify this:
const bitsInLastByte = (expectedBits mod 8)
when bitsInLastByte != 0:
# As an example, if there are 3 bits expected in the last byte,
# we calculate a bitmask equal to 11111000. If the input has any
# raised bits in range of the bitmask, this would be a violation
# of the size of the BitArray:
const forbiddenBitsMask = byte(byte(0xff) shl bitsInLastByte)
if (input[^1] and forbiddenBitsMask) != 0:
raiseIncorrectSize ResulType
2020-10-28 18:35:31 +00:00
func readSszValue*[T](input: openArray[byte],
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
val: var T) {.raisesssz.} =
mixin fromSszBytes, toSszType
2020-06-09 11:44:40 +00:00
template readOffsetUnchecked(n: int): uint32 {.used.}=
fromSszBytes(uint32, input.toOpenArray(n, n + offsetSize - 1))
template readOffset(n: int): int {.used.} =
let offset = readOffsetUnchecked(n)
2020-06-09 11:44:40 +00:00
if offset > input.len.uint32:
raise newException(MalformedSszError, "SSZ list element offset points past the end of the input")
2020-06-09 11:44:40 +00:00
int(offset)
2020-05-27 15:04:43 +00:00
when val is BitList:
if input.len == 0:
raise newException(MalformedSszError, "Invalid empty SSZ BitList value")
# Since our BitLists have an in-memory representation that precisely
# matches their SSZ encoding, we can deserialize them as regular Lists:
2020-05-27 15:04:43 +00:00
const maxExpectedSize = (val.maxLen div 8) + 1
type MatchingListType = List[byte, maxExpectedSize]
when false:
# TODO: Nim doesn't like this simple type coercion,
# we'll rely on `cast` for now (see below)
readSszValue(input, MatchingListType val)
else:
static:
# As a sanity check, we verify that the coercion is accepted by the compiler:
doAssert MatchingListType(val) is MatchingListType
readSszValue(input, cast[ptr MatchingListType](addr val)[])
2020-05-27 15:04:43 +00:00
let resultBytesCount = len bytes(val)
2020-05-27 15:04:43 +00:00
if bytes(val)[resultBytesCount - 1] == 0:
raise newException(MalformedSszError, "SSZ BitList is not properly terminated")
if resultBytesCount == maxExpectedSize:
checkForForbiddenBits(T, input, val.maxLen + 1)
elif val is HashList | HashArray:
readSszValue(input, val.data)
val.resetCache()
2020-05-27 15:04:43 +00:00
elif val is List|array:
type E = type val[0]
2020-06-09 11:44:40 +00:00
when E is byte:
2020-05-27 15:04:43 +00:00
val.setOutputSize input.len
if input.len > 0:
2020-05-27 15:04:43 +00:00
copyMem(addr val[0], unsafeAddr input[0], input.len)
elif isFixedSize(E):
const elemSize = fixedPortionSize(E)
if input.len mod elemSize != 0:
var ex = new SszSizeMismatchError
ex.deserializedType = cstring typetraits.name(T)
ex.actualSszSize = input.len
ex.elementSize = elemSize
raise ex
2020-05-27 15:04:43 +00:00
val.setOutputSize input.len div elemSize
for i in 0 ..< val.len:
let offset = i * elemSize
2020-05-27 15:04:43 +00:00
readSszValue(input.toOpenArray(offset, offset + elemSize - 1), val[i])
else:
if input.len == 0:
# This is an empty list.
# The default initialization of the return value is fine.
val.setOutputSize 0
return
2020-01-24 18:35:15 +00:00
elif input.len < offsetSize:
raise newException(MalformedSszError, "SSZ input of insufficient size")
var offset = readOffset 0
let resultLen = offset div offsetSize
if resultLen == 0:
# If there are too many elements, other constraints detect problems
# (not monotonically increasing, past end of input, or last element
# not matching up with its nextOffset properly)
raise newException(MalformedSszError, "SSZ list incorrectly encoded of zero length")
2020-05-27 15:04:43 +00:00
val.setOutputSize resultLen
for i in 1 ..< resultLen:
let nextOffset = readOffset(i * offsetSize)
2020-01-24 18:35:15 +00:00
if nextOffset <= offset:
raise newException(MalformedSszError, "SSZ list element offsets are not monotonically increasing")
else:
2020-05-27 15:04:43 +00:00
readSszValue(input.toOpenArray(offset, nextOffset - 1), val[i - 1])
offset = nextOffset
2020-05-27 15:04:43 +00:00
readSszValue(input.toOpenArray(offset, input.len - 1), val[resultLen - 1])
elif val is SingleMemberUnion:
readSszValue(input.toOpenArray(0, 0), val.selector)
if val.selector != 0'u8:
raise newException(MalformedSszError, "SingleMemberUnion selector must be 0")
readSszValue(input.toOpenArray(1, input.len - 1), val.value)
elif val is UintN|bool:
2020-05-28 12:36:37 +00:00
val = fromSszBytes(T, input)
2020-05-27 15:04:43 +00:00
elif val is BitArray:
if sizeof(val) != input.len:
raiseIncorrectSize(T)
checkForForbiddenBits(T, input, val.bits)
2020-05-27 15:04:43 +00:00
copyMem(addr val.bytes[0], unsafeAddr input[0], input.len)
elif val is object|tuple:
2020-06-09 11:44:40 +00:00
let inputLen = uint32 input.len
const minimallyExpectedSize = uint32 fixedPortionSize(T)
if inputLen < minimallyExpectedSize:
2020-01-24 18:35:15 +00:00
raise newException(MalformedSszError, "SSZ input of insufficient size")
2020-05-27 15:04:43 +00:00
enumInstanceSerializedFields(val, fieldName, field):
const boundingOffsets = getFieldBoundingOffsets(T, fieldName)
# type FieldType = type field # buggy
# For some reason, Nim gets confused about the alias here. This could be a
# generics caching issue caused by the use of distinct types. Such an
# issue is very scary in general.
# The bug can be seen with the two List[uint64, N] types that exist in
# the spec, with different N.
type SszType = type toSszType(declval type(field))
when isFixedSize(SszType):
const
startOffset = boundingOffsets[0]
endOffset = boundingOffsets[1]
else:
let
startOffset = readOffsetUnchecked(boundingOffsets[0])
2020-06-09 11:44:40 +00:00
endOffset = if boundingOffsets[1] == -1: inputLen
else: readOffsetUnchecked(boundingOffsets[1])
when boundingOffsets.isFirstOffset:
if startOffset != minimallyExpectedSize:
raise newException(MalformedSszError, "SSZ object dynamic portion starts at invalid offset")
2020-01-27 17:20:06 +00:00
if startOffset > endOffset:
2020-01-24 18:35:15 +00:00
raise newException(MalformedSszError, "SSZ field offsets are not monotonically increasing")
2020-06-09 11:44:40 +00:00
elif endOffset > inputLen:
2020-01-24 18:35:15 +00:00
raise newException(MalformedSszError, "SSZ field offset points past the end of the input")
elif startOffset < minimallyExpectedSize:
2020-04-16 19:21:28 +00:00
raise newException(MalformedSszError, "SSZ field offset points outside bounding offsets")
# TODO The extra type escaping here is a work-around for a Nim issue:
when type(field) is type(SszType):
2020-05-27 15:04:43 +00:00
readSszValue(
2020-06-09 11:44:40 +00:00
input.toOpenArray(int(startOffset), int(endOffset - 1)),
2020-05-27 15:04:43 +00:00
field)
else:
field = fromSszBytes(
type(field),
2020-06-09 11:44:40 +00:00
input.toOpenArray(int(startOffset), int(endOffset - 1)))
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
else:
unsupported T
# Identity conversions for core SSZ types
template toSszType*(v: auto): auto =
## toSszType converts a given value into one of the primitive types supported
## by SSZ - to add support for a custom type (for example a `distinct` type),
## add an overload for `toSszType` which converts it to one of the `SszType`
## types, as well as a `fromSszBytes`.
type T = type(v)
when T is SszType:
when T is Eth2Digest:
v.data
else:
v
else:
unsupported T