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

add EIP-7495 implementation: StableContainer #84

Open
wants to merge 64 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3339d33
add EIP-7495 implementation: `StableContainer`
etan-status May 4, 2024
63f71bd
add test file
etan-status May 4, 2024
cc3963e
fix
etan-status May 4, 2024
3919764
fixes
etan-status May 4, 2024
578baa4
cleanup
etan-status May 4, 2024
50ada22
fixes
etan-status May 4, 2024
ba1ed83
fix
etan-status May 4, 2024
3e91117
Merge branch 'master' into feat/eip-7495
etan-status May 5, 2024
338a47f
add `toOneBase` and support nested `{.sszVariant.}`
etan-status May 9, 2024
2b26f2a
fix variable size field reading
etan-status May 14, 2024
b3368c5
fix merkleization of `{.sszVariant.}` with a `Container` base
etan-status May 14, 2024
5b416b5
add more test vectors
etan-status May 14, 2024
bc045e1
Rename `Variant` -> `MerkleizeAs`
etan-status May 14, 2024
c03fad2
fix style check
etan-status May 14, 2024
37063f7
rename base type argument
etan-status May 15, 2024
df7b098
rename `MerkleizeAs` -> `Profile`
etan-status May 16, 2024
cf05533
Implement decoding for `Profile[B]`
etan-status May 20, 2024
d762a6d
`O == 0` case
etan-status May 20, 2024
befea9b
`Profile[B]` serialization
etan-status May 20, 2024
d8eb65d
Minimum size computation for `Profile[B]`
etan-status May 20, 2024
03ecb47
Nim 1.6 compat
etan-status May 21, 2024
446b03f
Multiproofs for `StableContainer[N]` and `Profile[B]`
etan-status May 22, 2024
5de55ba
Cleanup
etan-status May 22, 2024
75e2990
`sszSize` fixes for `StableContainer` / `Profile`
etan-status May 24, 2024
695d1c9
Merge branch 'master' into dev/etan/sz-7495
etan-status May 24, 2024
65fb0e7
Merge branch 'master' into dev/etan/sz-7495
etan-status Jun 6, 2024
08c3a09
Validity check for `StableContainer[N]`: `N > 0` and all fields `Opt`
etan-status Jun 6, 2024
3f2e736
Simplify `StableContainer[N]` deserialization: All fields `Opt`
etan-status Jun 6, 2024
cac3d42
Reduce compiler warnings
etan-status Jun 6, 2024
ef8691f
Simplify `StableContainer[N]` size computation: All fields `Opt`
etan-status Jun 6, 2024
25cce91
Simplify `StableContainer[N]` serialization: All fields `Opt`
etan-status Jun 6, 2024
51639b5
Simplify `StableContainer[N]` merkleization: All fields `Opt`
etan-status Jun 6, 2024
9074a5d
Remove `OneOf[B]`
etan-status Jun 6, 2024
f717ee7
Fix naming
etan-status Jun 6, 2024
88e7bf4
Cleanup `Profile[B]` deserialization and extend type validation
etan-status Jun 6, 2024
e80d9cd
Cleanup `Profile[B]` size computation
etan-status Jun 7, 2024
c446c2f
Cleanup `Profile[B]` size computation (2)
etan-status Jun 7, 2024
3bc37d2
Improve size computation efficiency
etan-status Jun 7, 2024
4051afb
Cleanup merkleization
etan-status Jun 7, 2024
d4003a1
Formatting
etan-status Jun 7, 2024
404d7ec
`stew/results` -> `results`
etan-status Jun 12, 2024
ebdd1bd
Complete the `hasCompatibleMerkleization` implementation for all types
etan-status Jun 12, 2024
c6c5702
Replace `fromProfileBase` with more generic `fromBase` implementation
etan-status Jun 12, 2024
320faa4
Lint
etan-status Jun 12, 2024
da7253c
Cleanup imports
etan-status Jun 12, 2024
4cc2948
Replace `toProfileBase` with more generic `toBase` implementation
etan-status Jun 13, 2024
ccd40ba
Variable length field fixes
etan-status Jun 13, 2024
a996899
Workaround list type bug
etan-status Jun 13, 2024
7e68023
Improve error message when trying invalid type coerceing
etan-status Jun 13, 2024
b4f7ed1
Fixes for profiles with optional fields
etan-status Jun 13, 2024
56319db
Workaround pragma bug with `Opt`
etan-status Jun 13, 2024
1236d8d
Cleanups
etan-status Jun 13, 2024
9f6fc63
Fix imports
etan-status Jun 13, 2024
2f19901
Fix test
etan-status Jun 13, 2024
b6b16f4
Merge branch 'master' into feat/eip-7495
etan-status Jun 26, 2024
776be02
Merge branch 'master' into dev/etan/sz-7495
etan-status Jul 25, 2024
9380db0
Merge branch 'master' into dev/etan/sz-7495
etan-status Aug 30, 2024
7c9955f
Merge branch 'master' into dev/etan/sz-7495
etan-status Sep 21, 2024
481b039
Extend tests for errors when coercing profiles
etan-status Oct 2, 2024
f57e399
Merge branch 'master' into dev/etan/sz-7495
etan-status Oct 9, 2024
a264b47
Positive 'nested surrounding container' tests
etan-status Oct 10, 2024
5ff61f4
Avoid clash between `macros.error` and `chronicles.error`
etan-status Oct 10, 2024
d9e1188
Merge branch 'master' into dev/etan/sz-7495
etan-status Oct 16, 2024
c9ad37a
Fix incorrect merge
etan-status Oct 16, 2024
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
135 changes: 114 additions & 21 deletions ssz_serialization.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ssz_serialization
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Copyright (c) 2018-2024 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).
Expand Down Expand Up @@ -89,8 +89,7 @@ func init*(T: type SszWriter, stream: OutputStream): T =

proc writeVarSizeType(w: var SszWriter, value: auto) {.gcsafe, raises: [IOError].}

func beginRecord*(w: var SszWriter, TT: type): auto =
type T = TT
func beginRecord*(w: var SszWriter, T: typedesc): auto =
when isFixedSize(T):
FixedSizedWriterCtx()
else:
Expand All @@ -107,7 +106,8 @@ template writeField*(w: var SszWriter,
when ctx is FixedSizedWriterCtx:
writeFixedSized(w.stream, toSszType(field))
else:
type FieldType = type toSszType(field)
# `FieldType` should be `type`: https://github.com/nim-lang/Nim/issues/23564
template FieldType: untyped = typeof toSszType(field)

when isFixedSize(FieldType):
writeFixedSized(ctx.fixedParts, toSszType(field))
Expand Down Expand Up @@ -155,6 +155,7 @@ proc writeElements[T](w: var SszWriter, value: openArray[T])
proc writeVarSizeType(w: var SszWriter, value: auto) {.raises: [IOError].} =
trs "STARTING VAR SIZE TYPE"

mixin toSszType
when value is HashArray|HashList:
writeVarSizeType(w, value.data)
elif value is array:
Expand All @@ -170,29 +171,92 @@ proc writeVarSizeType(w: var SszWriter, value: auto) {.raises: [IOError].} =
w.writeValue 1'u8
w.writeValue value.get
elif value is object|tuple:
when isCaseObject(type(value)):
when typeof(value).isStableContainer:
static: typeof(value).ensureIsValidStableContainer()
const N = typeof(value).getCustomPragmaVal(sszStableContainer)
var
activeFields: BitArray[N]
fieldIndex = 0
fixedSize = 0
value.enumInstanceSerializedFields(_ {.used.}, field):
if field.isSome:
template F: untyped = typeof(field).T
when F.isFixedSize:
fixedSize += static(F.fixedPortionSize)
else:
fixedSize += offsetSize
activeFields.setBit(fieldIndex)
inc fieldIndex

w.writeValue activeFields
block:
var ctx = VarSizedWriterCtx(
offset: fixedSize,
fixedParts: w.stream.delayFixedSizeWrite(fixedSize))
value.enumInstanceSerializedFields(_ {.used.}, field):
if field.isSome:
w.writeField ctx, astToStr(field), field.unsafeGet
w.endRecord ctx
elif typeof(value).isProfile:
static: typeof(value).ensureIsValidProfile()
const O = typeof(value).numOptionalFields
when O > 0:
var
optionalFields: BitArray[O]
optIndex = 0
var fixedSize = 0
value.enumInstanceSerializedFields(_ {.used.}, field):
when typeof(field) is Opt:
let hasField = field.isSome
if hasField:
optionalFields.setBit(optIndex)
inc optIndex
template F: untyped = typeof(field).T
else:
const hasField = true
template F: untyped = typeof(field)
if hasField:
when F.isFixedSize:
fixedSize += static(F.fixedPortionSize)
else:
fixedSize += offsetSize

when O > 0:
w.writeValue optionalFields
block:
var ctx = VarSizedWriterCtx(
offset: fixedSize,
fixedParts: w.stream.delayFixedSizeWrite(fixedSize))
value.enumInstanceSerializedFields(_ {.used.}, field):
when typeof(field) is Opt:
if field.isSome:
w.writeField ctx, astToStr(field), field.unsafeGet
else:
w.writeField ctx, astToStr(field), field
w.endRecord ctx
elif isCaseObject(type(value)):
isUnion(type(value))

trs "WRITING SSZ Union"

# toSszType for enum is kept local here as we don't want enums to
# serialize in general, only for object variants.
# TODO: This is not sufficiant as it will still allow to parse enum
# fields in the object variant itself. Will probably need some macro
# which enumerates the fields instead to take specific action on the
# discriminator.
template toSszType[E: enum](x: E): uint8 =
uint8(x)

# TODO: At this point, the assumption is a correct `isUnion` as described
# above.
enumerateSubFields(value, field):
type T = type toSszType(field)

when isFixedSize(T):
w.stream.writeFixedSized toSszType(field)
when typeof(field) is enum:
# toSszType for enum is kept local here as we don't want enums to
# serialize in general, only for object variants.
# TODO: This is not sufficiant as it will still allow to parse enum
# fields in the object variant itself. Will probably need some macro
# which enumerates the fields instead to take specific action on the
# discriminator.
w.stream.writeFixedSized uint8(field)
else:
w.writeVarSizeType toSszType(field)
type T = type toSszType(field)

when isFixedSize(T):
w.stream.writeFixedSized toSszType(field)
else:
w.writeVarSizeType toSszType(field)
else:
trs "WRITING OBJECT"
var ctx = beginRecord(w, type value)
Expand Down Expand Up @@ -222,7 +286,9 @@ func sszSizeForVarSizeList[T](value: openArray[T]): int {.gcsafe, raises:[].} =

func sszSize*(value: auto): int {.gcsafe, raises:[].} =
mixin toSszType
type T = type toSszType(value)

# `T` should be `type`: https://github.com/nim-lang/Nim/issues/23564
template T: untyped = typeof toSszType(value)

when isFixedSize(T):
anonConst fixedPortionSize(T)
Expand All @@ -248,7 +314,34 @@ func sszSize*(value: auto): int {.gcsafe, raises:[].} =
0

elif T is object|tuple:
when T.isCaseObject():
when T.isStableContainer:
static: T.ensureIsValidStableContainer()
var total = static(T.fixedPortionSize)
value.enumInstanceSerializedFields(_ {.used.}, field):
if field.isSome:
template F: untyped = typeof(field).T
when F.isFixedSize:
total += static(F.fixedPortionSize)
else:
total += offsetSize + field.unsafeGet.sszSize
total
elif T.isProfile:
static: typeof(value).ensureIsValidProfile()
var total = static(T.fixedPortionSize)
when not T.isFixedSize:
value.enumInstanceSerializedFields(_ {.used.}, field):
when typeof(field) is Opt:
if field.isSome:
template F: untyped = typeof(field).T
when F.isFixedSize:
total += static(F.fixedPortionSize)
else:
total += offsetSize + field.unsafeGet.sszSize
else:
when not typeof(field).isFixedSize:
total += field.sszSize
total
elif T.isCaseObject():
isUnion(T)
unionSize(value)
else:
Expand Down
160 changes: 157 additions & 3 deletions ssz_serialization/codec.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ssz_serialization
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Copyright (c) 2018-2024 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).
Expand Down Expand Up @@ -237,11 +237,19 @@ macro initSszUnionImpl(RecordType: type, input: openArray[byte]): untyped =
func initSszUnion(T: type, input: openArray[byte]): T {.raises: [SszError].} =
initSszUnionImpl(T, input)

func read(F: typedesc, input: openArray[byte]): F {.raises: [SszError].} =
when compiles(F.fromSszBytes(input)):
F.fromSszBytes(input)
else:
var f: F
readSszValue(input, f)
f

proc readSszValue*[T](
input: openArray[byte], val: var T) {.raises: [SszError].} =
mixin fromSszBytes, toSszType, readSszValue

template readOffsetUnchecked(n: int): uint32 {.used.}=
template readOffsetUnchecked(n: int): uint32 {.used.} =
fromSszBytes(uint32, input.toOpenArray(n, n + offsetSize - 1))

template readOffset(n: int): int {.used.} =
Expand Down Expand Up @@ -449,7 +457,153 @@ proc readSszValue*[T](
copyMem(addr val.bytes[0], unsafeAddr input[0], input.len)

elif val is object|tuple:
when isCaseObject(T):
when T.isStableContainer:
static: T.ensureIsValidStableContainer()
const N = T.getCustomPragmaVal(sszStableContainer)

let inputLen = input.len
const numPrefixBytes = BitArray[N].fixedPortionSize
if inputLen < numPrefixBytes:
raiseMalformedSszError(T, "Scope too small for " &
"`{.sszStableContainer: " & $N & ".}` active fields")
let activeFields = BitArray[N].read(
input.toOpenArray(0, numPrefixBytes - 1))

for fieldIndex in T.totalSerializedFields ..< N:
if activeFields[fieldIndex]:
raiseMalformedSszError(T, "Unknown field index " & $fieldIndex)

val.reset()
var
fieldIndex = 0
offset = numPrefixBytes
dynFieldOffsets: seq[int]
val.enumInstanceSerializedFields(fieldName, field):
if activeFields[fieldIndex]:
template F: untyped = typeof(field).T
when F.isFixedSize:
const fieldSize = F.fixedPortionSize
if inputLen - offset < fieldSize:
raiseMalformedSszError(T, "Scope too small for " &
"`" & fieldName & "` of type `" & $F & "`")
field.ok F.read(
input.toOpenArray(offset, offset + fieldSize - 1))
offset += fieldSize
else:
if inputLen - offset < offsetSize:
raiseMalformedSszError(T, "Scope too small for " &
"`" & fieldName & "` offset")
dynFieldOffsets.add readOffset(offset)
if dynFieldOffsets[^1] > inputLen - numPrefixBytes:
raiseMalformedSszError(T, "Field offset past end")
if dynFieldOffsets.len > 1 and
dynFieldOffsets[^1] < dynFieldOffsets[^2]:
raiseMalformedSszError(T, "Field offset not larger than previous")
offset += offsetSize
inc fieldIndex
if dynFieldOffsets.len > 0:
dynFieldOffsets.add inputLen - numPrefixBytes
fieldIndex = 0
var i = 0
val.enumInstanceSerializedFields(_ {.used.}, field):
template F: untyped = typeof(field).T
when not F.isFixedSize:
if activeFields[fieldIndex]:
if dynFieldOffsets[i] != offset - numPrefixBytes:
raiseMalformedSszError(T, "Field offset invalid")
let fieldSize = dynFieldOffsets[i + 1] - dynFieldOffsets[i]
field.ok F.read(
input.toOpenArray(offset, offset + fieldSize - 1))
offset += fieldSize
inc i
inc fieldIndex
doAssert i == (dynFieldOffsets.len - 1)
if offset != inputLen:
raiseMalformedSszError(T, "Unexpected extra data after object")
elif T.isProfile:
static: T.ensureIsValidProfile()
const O = T.numOptionalFields

let inputLen = input.len
when O > 0:
# `B` should be `type`: https://github.com/nim-lang/Nim/issues/23564
template B: untyped = T.getCustomPragmaVal(sszProfile)
const numPrefixBytes = BitArray[O].fixedPortionSize
if inputLen < numPrefixBytes:
raiseMalformedSszError(T, "Scope too small for " &
"`{.sszProfile: " & $B & ".}` optional fields")
let optionalFields = BitArray[O].read(
input.toOpenArray(0, numPrefixBytes - 1))
else:
const numPrefixBytes = 0

val.reset()
var
optIndex = 0
offset = numPrefixBytes
dynFieldOffsets: seq[int]
val.enumInstanceSerializedFields(fieldName, field):
when typeof(field) is Opt:
let hasField = optionalFields[optIndex]
inc optIndex
template F: untyped = typeof(field).T
else:
const hasField = true
template F: untyped = typeof(field)
if hasField:
when F.isFixedSize:
const fieldSize = F.fixedPortionSize
if inputLen - offset < fieldSize:
raiseMalformedSszError(T, "Scope too small for " &
"`" & fieldName & "` of type `" & $F & "`")
when typeof(field) is Opt:
field.ok F.read(
input.toOpenArray(offset, offset + fieldSize - 1))
else:
readSszValue(
input.toOpenArray(offset, offset + fieldSize - 1), field)
offset += fieldSize
else:
if inputLen - offset < offsetSize:
raiseMalformedSszError(T, "Scope too small for " &
"`" & fieldName & "` offset")
dynFieldOffsets.add readOffset(offset)
if dynFieldOffsets[^1] > inputLen - numPrefixBytes:
raiseMalformedSszError(T, "Field offset past end")
if dynFieldOffsets.len > 1 and
dynFieldOffsets[^1] < dynFieldOffsets[^2]:
raiseMalformedSszError(T, "Field offset not larger than previous")
offset += offsetSize
doAssert optIndex == O
if dynFieldOffsets.len > 0:
dynFieldOffsets.add inputLen - numPrefixBytes
optIndex = 0
var i = 0
val.enumInstanceSerializedFields(_ {.used.}, field):
when typeof(field) is Opt:
let hasField = optionalFields[optIndex]
inc optIndex
template F: untyped = typeof(field).T
else:
const hasField = true
template F: untyped = typeof(field)
if hasField:
when not F.isFixedSize:
if dynFieldOffsets[i] != offset - numPrefixBytes:
raiseMalformedSszError(T, "Field offset invalid")
let fieldSize = dynFieldOffsets[i + 1] - dynFieldOffsets[i]
when typeof(field) is Opt:
field.ok F.read(
input.toOpenArray(offset, offset + fieldSize - 1))
else:
readSszValue(
input.toOpenArray(offset, offset + fieldSize - 1), field)
offset += fieldSize
inc i
doAssert i == (dynFieldOffsets.len - 1)
if offset != inputLen:
raiseMalformedSszError(T, "Unexpected extra data after object")
elif isCaseObject(T):
isUnion(type(val))
val = initSszUnion(type(val), input)
else:
Expand Down
Loading