diff --git a/lib/spec/runners/ssz_generic.ex b/lib/spec/runners/ssz_generic.ex index 75a99e13b..28cb8a00b 100644 --- a/lib/spec/runners/ssz_generic.ex +++ b/lib/spec/runners/ssz_generic.ex @@ -49,18 +49,28 @@ defmodule SszGenericTestRunner do defp handle_case("valid", schema, real_serialized, testcase) do case_dir = SpecTestCase.dir(testcase) - expected = + expected_value = YamlElixir.read_from_file!(case_dir <> "/value.yaml") |> SpecTestUtils.sanitize_yaml() - assert_ssz("valid", schema, real_serialized, expected) + %{root: expected_root} = + YamlElixir.read_from_file!(case_dir <> "/meta.yaml") + |> SpecTestUtils.sanitize_yaml() + + assert_ssz("valid", schema, real_serialized, expected_value, expected_root) end defp handle_case("invalid", schema, real_serialized, _testcase) do assert_ssz("invalid", schema, real_serialized) end - defp assert_ssz("valid", {:container, module}, real_serialized, real_deserialized) do + defp assert_ssz( + "valid", + {:container, module}, + real_serialized, + real_deserialized, + _hash_tree_root + ) do real_struct = struct!(module, real_deserialized) {:ok, deserialized} = SszEx.decode(real_serialized, module) assert deserialized == real_struct @@ -68,13 +78,17 @@ defmodule SszGenericTestRunner do assert serialized == real_serialized end - defp assert_ssz("valid", schema, real_serialized, real_deserialized) do + defp assert_ssz("valid", schema, real_serialized, real_deserialized, expected_hash_tree_root) do {:ok, deserialized} = SszEx.decode(real_serialized, schema) assert deserialized == real_deserialized {:ok, serialized} = SszEx.encode(real_deserialized, schema) assert serialized == real_serialized + + actual_hash_tree_root = SszEx.hash_tree_root!(real_deserialized, schema) + + assert actual_hash_tree_root == expected_hash_tree_root end defp assert_ssz("invalid", schema, real_serialized) do diff --git a/lib/ssz.ex b/lib/ssz.ex index d015e1d1f..633be56dc 100644 --- a/lib/ssz.ex +++ b/lib/ssz.ex @@ -1,6 +1,6 @@ defmodule Ssz do @moduledoc """ - SimpleSerialize (SSZ) serialization and deserialization. + SimpleSerialize (SSZ) serialization, deserialization and merkleization. """ use Rustler, otp_app: :lambda_ethereum_consensus, crate: "ssz_nif" diff --git a/lib/ssz_ex.ex b/lib/ssz_ex.ex index 1add539de..8ff625942 100644 --- a/lib/ssz_ex.ex +++ b/lib/ssz_ex.ex @@ -7,6 +7,8 @@ defmodule LambdaEthereumConsensus.SszEx do ### Public API ################# + @bits_per_chunk 256 + @spec hash(iodata()) :: binary() def hash(data), do: :crypto.hash(:sha256, data) @@ -36,6 +38,12 @@ defmodule LambdaEthereumConsensus.SszEx do def decode(binary, module) when is_atom(module), do: decode_container(binary, module) + @spec hash_tree_root!(boolean, atom) :: SszTypes.root() + def hash_tree_root!(value, :bool), do: pack(value) + + @spec hash_tree_root!(non_neg_integer, {:int, non_neg_integer}) :: SszTypes.root() + def hash_tree_root!(value, {:int, size}), do: pack(value, size) + ################# ### Private functions ################# @@ -398,4 +406,16 @@ defmodule LambdaEthereumConsensus.SszEx do |> Enum.map(fn {_, schema} -> variable_size?(schema) end) |> Enum.any?() end + + defp pack(value, size) when is_integer(value) and value >= 0 do + pad = @bits_per_chunk - size + <> + end + + defp pack(value) when is_boolean(value) do + case value do + true -> <<1::@bits_per_chunk-little>> + false -> <<0::@bits_per_chunk>> + end + end end