Skip to content

Commit 1aa9340

Browse files
f3r10Godspower-Eze
andauthored
feat: enable ssz generic spec tests (#683)
Co-authored-by: Godspower Eze <[email protected]> Co-authored-by: Godspower Eze <[email protected]>
1 parent 3da8c9e commit 1aa9340

File tree

4 files changed

+236
-128
lines changed

4 files changed

+236
-128
lines changed

lib/ssz_ex.ex

Lines changed: 166 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ defmodule LambdaEthereumConsensus.SszEx do
33
SSZ library in Elixir
44
"""
55
alias LambdaEthereumConsensus.Utils.BitList
6-
alias LambdaEthereumConsensus.Utils.BitVector
76
import alias LambdaEthereumConsensus.Utils.BitVector
7+
import Bitwise
88
alias LambdaEthereumConsensus.Utils.ZeroHashes
99

1010
#################
@@ -34,8 +34,11 @@ defmodule LambdaEthereumConsensus.SszEx do
3434
else: encode_fixed_size_list(list, basic_type, size)
3535
end
3636

37-
def encode(vector, {:vector, basic_type, size}),
38-
do: encode_fixed_size_list(vector, basic_type, size)
37+
def encode(vector, {:vector, basic_type, size}) do
38+
if variable_size?(basic_type),
39+
do: encode_variable_size_list(vector, basic_type, size),
40+
else: encode_fixed_size_list(vector, basic_type, size)
41+
end
3942

4043
def encode(value, {:bitlist, max_size}) when is_bitstring(value),
4144
do: encode_bitlist(value, max_size)
@@ -56,18 +59,26 @@ defmodule LambdaEthereumConsensus.SszEx do
5659
def decode(binary, {:list, basic_type, size}) do
5760
if variable_size?(basic_type),
5861
do: decode_variable_list(binary, basic_type, size),
59-
else: decode_list(binary, basic_type, size)
62+
else: decode_fixed_list(binary, basic_type, size)
6063
end
6164

62-
def decode(binary, {:vector, basic_type, size}), do: decode_list(binary, basic_type, size)
65+
def decode(binary, {:vector, basic_type, size}) do
66+
if variable_size?(basic_type),
67+
do: decode_variable_list(binary, basic_type, size),
68+
else: decode_fixed_vector(binary, basic_type, size)
69+
end
6370

6471
def decode(value, {:bitlist, max_size}) when is_bitstring(value),
6572
do: decode_bitlist(value, max_size)
6673

6774
def decode(value, {:bitvector, size}) when is_bitstring(value),
6875
do: decode_bitvector(value, size)
6976

70-
def decode(binary, module) when is_atom(module), do: decode_container(binary, module)
77+
def decode(binary, module) when is_atom(module) do
78+
if variable_size?(module),
79+
do: decode_variable_container(binary, module),
80+
else: decode_fixed_container(binary, module)
81+
end
7182

7283
@spec hash_tree_root!(boolean, atom) :: Types.root()
7384
def hash_tree_root!(value, :bool), do: pack(value, :bool)
@@ -274,13 +285,16 @@ defmodule LambdaEthereumConsensus.SszEx do
274285
defp encode_bool(true), do: {:ok, "\x01"}
275286
defp encode_bool(false), do: {:ok, "\x00"}
276287

277-
defp decode_uint(binary, size) do
288+
defp decode_uint(binary, size) when bit_size(binary) == size do
278289
<<element::integer-size(size)-little, _rest::bitstring>> = binary
279290
{:ok, element}
280291
end
281292

293+
defp decode_uint(_binary, size), do: {:error, "invalid byte size #{inspect(size)}"}
294+
282295
defp decode_bool("\x01"), do: {:ok, true}
283296
defp decode_bool("\x00"), do: {:ok, false}
297+
defp decode_bool(_), do: {:error, "invalid bool value"}
284298

285299
defp encode_fixed_size_list(list, _basic_type, max_size) when length(list) > max_size,
286300
do: {:error, "invalid max_size of list"}
@@ -348,7 +362,7 @@ defmodule LambdaEthereumConsensus.SszEx do
348362
end
349363
end
350364

351-
defp decode_bitlist(bit_list, max_size) do
365+
defp decode_bitlist(bit_list, max_size) when bit_size(bit_list) > 0 do
352366
num_bytes = byte_size(bit_list)
353367
{decoded, len} = BitList.new(bit_list)
354368

@@ -367,26 +381,70 @@ defmodule LambdaEthereumConsensus.SszEx do
367381
end
368382
end
369383

370-
defp decode_bitvector(bit_vector, size) when bit_size(bit_vector) == size,
371-
do: {:ok, BitVector.new(bit_vector, size)}
384+
defp decode_bitlist(_bit_list, _max_size), do: {:error, "invalid bitlist"}
372385

373-
defp decode_bitvector(_bit_vector, _size), do: {:error, "invalid bit_vector length"}
386+
defp decode_bitvector(bit_vector, size) do
387+
num_bytes = byte_size(bit_vector)
374388

375-
defp decode_list(binary, basic_type, size) do
389+
cond do
390+
bit_size(bit_vector) == 0 ->
391+
{:error, "ExcessBits"}
392+
393+
num_bytes != max(1, div(size + 7, 8)) ->
394+
{:error, "InvalidByteCount"}
395+
396+
true ->
397+
case bit_vector do
398+
# Padding bits are clear
399+
<<_first::binary-size(num_bytes - 1), 0::size(8 - rem(size, 8) &&& 7),
400+
_rest::bitstring>> ->
401+
{:ok, BitVector.new(bit_vector, size)}
402+
403+
_else ->
404+
{:error, "ExcessBits"}
405+
end
406+
end
407+
end
408+
409+
defp decode_fixed_list(binary, basic_type, size) do
376410
fixed_size = get_fixed_size(basic_type)
377411

378-
with {:ok, decoded_list} = result <-
379-
binary
380-
|> decode_chunk(fixed_size, basic_type)
381-
|> flatten_results() do
382-
if length(decoded_list) > size do
383-
{:error, "invalid max_size of list"}
384-
else
385-
result
386-
end
412+
with {:ok, decoded_list} = result <- decode_fixed_collection(binary, fixed_size, basic_type),
413+
:ok <- check_valid_list_size_after_decode(size, length(decoded_list)) do
414+
result
415+
end
416+
end
417+
418+
defp decode_fixed_vector(binary, basic_type, size) do
419+
fixed_size = get_fixed_size(basic_type)
420+
421+
with :ok <- check_valid_vector_size_prev_decode(fixed_size, size, binary),
422+
{:ok, decoded_vector} = result <-
423+
decode_fixed_collection(binary, fixed_size, basic_type),
424+
:ok <- check_valid_vector_size_after_decode(size, length(decoded_vector)) do
425+
result
387426
end
388427
end
389428

429+
def check_valid_vector_size_prev_decode(fixed_size, size, binary)
430+
when fixed_size * size == byte_size(binary),
431+
do: :ok
432+
433+
def check_valid_vector_size_prev_decode(_fixed_size, _size, _binary),
434+
do: {:error, "Invalid vector size"}
435+
436+
def check_valid_vector_size_after_decode(size, decoded_size)
437+
when decoded_size == size and decoded_size > 0,
438+
do: :ok
439+
440+
def check_valid_vector_size_after_decode(_size, _decoded_size),
441+
do: {:error, "invalid vector decoded size"}
442+
443+
def check_valid_list_size_after_decode(size, decoded_size) when decoded_size <= size, do: :ok
444+
445+
def check_valid_list_size_after_decode(_size, _decoded_size),
446+
do: {:error, "invalid max_size of list"}
447+
390448
defp decode_variable_list(binary, _, _) when byte_size(binary) == 0 do
391449
{:ok, []}
392450
end
@@ -407,7 +465,7 @@ defmodule LambdaEthereumConsensus.SszEx do
407465
first_offset < @bytes_per_length_offset do
408466
{:error, "InvalidListFixedBytesLen"}
409467
else
410-
with {:ok, first_offset} <-
468+
with :ok <-
411469
sanitize_offset(first_offset, nil, byte_size(binary), first_offset) do
412470
decode_variable_list_elements(
413471
num_elements,
@@ -448,7 +506,7 @@ defmodule LambdaEthereumConsensus.SszEx do
448506
) do
449507
<<next_offset::integer-32-little, rest_bytes::bitstring>> = acc_rest_bytes
450508

451-
with {:ok, next_offset} <-
509+
with :ok <-
452510
sanitize_offset(next_offset, offset, byte_size(binary), first_offset) do
453511
part = :binary.part(binary, offset, next_offset - offset)
454512

@@ -467,7 +525,7 @@ defmodule LambdaEthereumConsensus.SszEx do
467525
defp encode_container(container, schemas) do
468526
{fixed_size_values, fixed_length, variable_values} = analyze_schemas(container, schemas)
469527

470-
with {:ok, variable_parts} <- encode_schemas(variable_values),
528+
with {:ok, variable_parts} <- encode_schemas(Enum.reverse(variable_values)),
471529
offsets = calculate_offsets(variable_parts, fixed_length),
472530
variable_length =
473531
Enum.reduce(variable_parts, 0, fn part, acc -> byte_size(part) + acc end),
@@ -524,48 +582,87 @@ defmodule LambdaEthereumConsensus.SszEx do
524582
defp replace_offset(element, {acc_fixed_list, acc_offsets_list}),
525583
do: {[element | acc_fixed_list], acc_offsets_list}
526584

527-
defp decode_container(binary, module) do
585+
defp decode_variable_container(binary, module) do
528586
schemas = module.schema()
529587
fixed_length = get_fixed_length(schemas)
530-
<<fixed_binary::binary-size(fixed_length), variable_binary::bitstring>> = binary
531588

532-
with {:ok, fixed_parts, offsets} <- decode_fixed_section(fixed_binary, schemas, fixed_length),
533-
{:ok, variable_parts} <- decode_variable_section(variable_binary, offsets) do
589+
with :ok <- sanitize_offset(fixed_length, nil, byte_size(binary), nil),
590+
<<fixed_binary::binary-size(fixed_length), variable_binary::bitstring>> = binary,
591+
{:ok, fixed_parts, offsets, items_index} <-
592+
decode_fixed_section(fixed_binary, schemas, fixed_length),
593+
:ok <- check_first_offset(offsets, items_index, byte_size(binary)),
594+
{:ok, variable_parts} <- decode_variable_section(binary, variable_binary, offsets) do
534595
{:ok, struct!(module, fixed_parts ++ variable_parts)}
535596
end
536597
end
537598

538-
defp decode_variable_section(binary, offsets) do
599+
defp decode_fixed_container(binary, module) do
600+
schemas = module.schema()
601+
fixed_length = get_fixed_length(schemas)
602+
603+
with {:ok, fixed_parts, _offsets, items_index} <-
604+
decode_fixed_section(binary, schemas, fixed_length),
605+
:ok <- check_byte_len(items_index, byte_size(binary)) do
606+
{:ok, struct!(module, fixed_parts)}
607+
end
608+
end
609+
610+
defp check_first_offset([{offset, _} | _rest], items_index, _binary_size) do
611+
cond do
612+
offset < items_index -> {:error, "OffsetIntoFixedPortion"}
613+
offset > items_index -> {:error, "OffsetSkipsVariableBytes"}
614+
true -> :ok
615+
end
616+
end
617+
618+
defp check_byte_len(items_index, binary_size)
619+
when items_index == binary_size,
620+
do: :ok
621+
622+
defp check_byte_len(_items_index, _binary_size),
623+
do: {:error, "InvalidByteLength"}
624+
625+
defp decode_variable_section(full_binary, binary, offsets) do
539626
offsets
540627
|> Enum.chunk_every(2, 1)
541-
|> Enum.reduce({binary, []}, fn
628+
|> Enum.reduce_while({binary, []}, fn
542629
[{offset, {key, schema}}, {next_offset, _}], {rest_bytes, acc_variable_parts} ->
543-
size = next_offset - offset
544-
<<chunk::binary-size(size), rest::bitstring>> = rest_bytes
545-
{rest, [{key, decode(chunk, schema)} | acc_variable_parts]}
630+
case sanitize_offset(next_offset, offset, byte_size(full_binary), nil) do
631+
:ok ->
632+
size = next_offset - offset
633+
<<chunk::binary-size(size), rest::bitstring>> = rest_bytes
634+
{:cont, {rest, [{key, decode(chunk, schema)} | acc_variable_parts]}}
635+
636+
error ->
637+
{:halt, {<<>>, [{key, error} | acc_variable_parts]}}
638+
end
546639

547640
[{_offset, {key, schema}}], {rest_bytes, acc_variable_parts} ->
548-
{<<>>, [{key, decode(rest_bytes, schema)} | acc_variable_parts]}
641+
{:cont, {<<>>, [{key, decode(rest_bytes, schema)} | acc_variable_parts]}}
549642
end)
550643
|> then(fn {<<>>, variable_parts} ->
551644
flatten_container_results(variable_parts)
552645
end)
553646
end
554647

555-
defp decode_fixed_section(binary, schemas, fixed_length) do
648+
defp decode_fixed_section(binary, schemas, _fixed_length) do
556649
schemas
557-
|> Enum.reduce({binary, [], []}, fn {key, schema}, {binary, fixed_parts, offsets} ->
650+
|> Enum.reduce({binary, [], [], 0}, fn {key, schema},
651+
{binary, fixed_parts, offsets, items_index} ->
558652
if variable_size?(schema) do
559653
<<offset::integer-size(@offset_bits)-little, rest::bitstring>> = binary
560-
{rest, fixed_parts, [{offset - fixed_length, {key, schema}} | offsets]}
654+
655+
{rest, fixed_parts, [{offset, {key, schema}} | offsets],
656+
items_index + @bytes_per_length_offset}
561657
else
562658
ssz_fixed_len = get_fixed_size(schema)
563659
<<chunk::binary-size(ssz_fixed_len), rest::bitstring>> = binary
564-
{rest, [{key, decode(chunk, schema)} | fixed_parts], offsets}
660+
{rest, [{key, decode(chunk, schema)} | fixed_parts], offsets, items_index + ssz_fixed_len}
565661
end
566662
end)
567-
|> then(fn {_rest_bytes, fixed_parts, offsets} ->
663+
|> then(fn {_rest_bytes, fixed_parts, offsets, items_index} ->
568664
Tuple.append(flatten_container_results(fixed_parts), Enum.reverse(offsets))
665+
|> Tuple.append(items_index)
569666
end)
570667
end
571668

@@ -582,35 +679,47 @@ defmodule LambdaEthereumConsensus.SszEx do
582679
end
583680

584681
# https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view
585-
defp sanitize_offset(offset, previous_offset, num_bytes, num_fixed_bytes) do
682+
defp sanitize_offset(offset, previous_offset, num_bytes, nil) do
586683
cond do
587-
offset < num_fixed_bytes ->
588-
{:error, "OffsetIntoFixedPortion"}
589-
590-
previous_offset == nil && offset != num_fixed_bytes ->
591-
{:error, "OffsetSkipsVariableBytes"}
592-
593684
offset > num_bytes ->
594685
{:error, "OffsetOutOfBounds"}
595686

596687
previous_offset != nil && previous_offset > offset ->
597688
{:error, "OffsetsAreDecreasing"}
598689

599690
true ->
600-
{:ok, offset}
691+
:ok
601692
end
602693
end
603694

604-
defp decode_chunk(binary, chunk_size, basic_type) do
605-
decode_chunk(binary, chunk_size, basic_type, [])
695+
defp sanitize_offset(offset, previous_offset, _num_bytes, num_fixed_bytes) do
696+
cond do
697+
offset < num_fixed_bytes ->
698+
{:error, "OffsetIntoFixedPortion"}
699+
700+
previous_offset == nil && offset != num_fixed_bytes ->
701+
{:error, "OffsetSkipsVariableBytes"}
702+
703+
true ->
704+
:ok
705+
end
706+
end
707+
708+
defp decode_fixed_collection(binary, chunk_size, basic_type) do
709+
decode_fixed_collection(binary, chunk_size, basic_type, [])
606710
|> Enum.reverse()
711+
|> flatten_results()
607712
end
608713

609-
defp decode_chunk(<<>>, _chunk_size, _basic_type, results), do: results
714+
defp decode_fixed_collection(<<>>, _chunk_size, _basic_type, results), do: results
715+
716+
defp decode_fixed_collection(binary, chunk_size, _basic_type, results)
717+
when byte_size(binary) < chunk_size,
718+
do: [{:error, "InvalidByteLength"} | results]
610719

611-
defp decode_chunk(binary, chunk_size, basic_type, results) do
720+
defp decode_fixed_collection(binary, chunk_size, basic_type, results) do
612721
<<element::binary-size(chunk_size), rest::bitstring>> = binary
613-
decode_chunk(rest, chunk_size, basic_type, [decode(element, basic_type) | results])
722+
decode_fixed_collection(rest, chunk_size, basic_type, [decode(element, basic_type) | results])
614723
end
615724

616725
defp flatten_results(results) do
@@ -645,7 +754,8 @@ defmodule LambdaEthereumConsensus.SszEx do
645754
defp get_fixed_size(:bool), do: 1
646755
defp get_fixed_size({:int, size}), do: div(size, @bits_per_byte)
647756
defp get_fixed_size({:bytes, size}), do: size
648-
defp get_fixed_size({:vector, _, size}), do: size
757+
defp get_fixed_size({:vector, basic_type, size}), do: size * get_fixed_size(basic_type)
758+
defp get_fixed_size({:bitvector, _}), do: 1
649759

650760
defp get_fixed_size(module) when is_atom(module) do
651761
schemas = module.schema()
@@ -656,10 +766,12 @@ defmodule LambdaEthereumConsensus.SszEx do
656766
end
657767

658768
defp variable_size?({:list, _, _}), do: true
659-
defp variable_size?({:vector, _, _}), do: false
660769
defp variable_size?(:bool), do: false
661770
defp variable_size?({:int, _}), do: false
662771
defp variable_size?({:bytes, _}), do: false
772+
defp variable_size?({:bitlist, _}), do: true
773+
defp variable_size?({:bitvector, _}), do: false
774+
defp variable_size?({:vector, basic_type, _}), do: variable_size?(basic_type)
663775

664776
defp variable_size?(module) when is_atom(module) do
665777
module.schema()

test/spec/runners/helpers/ssz_static_containers/complex_test_struct.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ defmodule Helpers.SszStaticContainers.ComplexTestStruct do
3535
{:A, {:int, 16}},
3636
{:B, {:list, {:int, 16}, 1024}},
3737
{:C, {:int, 8}},
38-
{:D, {:bitlist, 256}},
38+
{:D, {:list, {:int, 8}, 256}},
3939
{:E, VarTestStruct},
4040
{:F, {:vector, FixedTestStruct, 4}},
4141
{:G, {:vector, VarTestStruct, 2}}

0 commit comments

Comments
 (0)