diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 284a23444..11b9c2322 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,8 @@ jobs: uses: Swatinem/rust-cache@v2 with: workspaces: ${{ env.RUST_WORKSPACES }} + - name: Compile Elixir (Warnings as errors) + run: mix compile --warnings-as-errors - name: Retrieve PLT Cache uses: actions/cache@v1 id: plt-cache diff --git a/.spectest_version b/.spectest_version index 18fa8e74f..0a7905b0e 100644 --- a/.spectest_version +++ b/.spectest_version @@ -1 +1 @@ -v1.3.0 +v1.4.0-alpha.0 diff --git a/lib/container.ex b/lib/container.ex new file mode 100644 index 000000000..8ce164a3c --- /dev/null +++ b/lib/container.ex @@ -0,0 +1,11 @@ +defmodule LambdaEthereumConsensus.Container do + @moduledoc """ + Container for SSZ + """ + + @doc """ + List of ordered {key, schema} tuples. + It specifies both the serialization order and the schema for each key in the map. + """ + @callback schema() :: [{atom, any}] +end diff --git a/lib/lambda_ethereum_consensus/state_transition/accessors.ex b/lib/lambda_ethereum_consensus/state_transition/accessors.ex index 54603a919..abf4403ea 100644 --- a/lib/lambda_ethereum_consensus/state_transition/accessors.ex +++ b/lib/lambda_ethereum_consensus/state_transition/accessors.ex @@ -291,8 +291,13 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do Return the base reward for the validator defined by ``index`` with respect to the current ``state``. """ @spec get_base_reward(BeaconState.t(), SszTypes.validator_index()) :: SszTypes.gwei() - def get_base_reward(state, index) do + def get_base_reward(%BeaconState{} = state, index) do validator = Enum.at(state.validators, index) + get_base_reward(validator, get_base_reward_per_increment(state)) + end + + @spec get_base_reward(SszTypes.Validator.t(), SszTypes.gwei()) :: SszTypes.gwei() + def get_base_reward(%SszTypes.Validator{} = validator, base_reward_per_increment) do effective_balance = validator.effective_balance increments = @@ -301,7 +306,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do ChainSpec.get("EFFECTIVE_BALANCE_INCREMENT") ) - increments * get_base_reward_per_increment(state) + increments * base_reward_per_increment end @doc """ @@ -378,7 +383,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do @spec get_block_root_at_slot(BeaconState.t(), SszTypes.slot()) :: {:ok, SszTypes.root()} | {:error, binary()} def get_block_root_at_slot(state, slot) do - if slot < state.slot && state.slot <= slot + ChainSpec.get("SLOTS_PER_HISTORICAL_ROOT") do + if slot < state.slot and state.slot <= slot + ChainSpec.get("SLOTS_PER_HISTORICAL_ROOT") do root = Enum.at(state.block_roots, rem(slot, ChainSpec.get("SLOTS_PER_HISTORICAL_ROOT"))) {:ok, root} else @@ -498,11 +503,13 @@ defmodule LambdaEthereumConsensus.StateTransition.Accessors do @spec get_total_balance(BeaconState.t(), Enumerable.t(SszTypes.validator_index())) :: SszTypes.gwei() def get_total_balance(state, indices) do + indices = MapSet.new(indices) + total_balance = - indices - |> Stream.map(fn index -> - Map.get(Enum.at(state.validators, index), :effective_balance, 0) - end) + state.validators + |> Stream.with_index() + |> Stream.filter(fn {_, index} -> MapSet.member?(indices, index) end) + |> Stream.map(fn {%SszTypes.Validator{effective_balance: n}, _} -> n end) |> Enum.sum() max(ChainSpec.get("EFFECTIVE_BALANCE_INCREMENT"), total_balance) diff --git a/lib/lambda_ethereum_consensus/state_transition/epoch_processing.ex b/lib/lambda_ethereum_consensus/state_transition/epoch_processing.ex index 21f5c7e1a..9773582b7 100644 --- a/lib/lambda_ethereum_consensus/state_transition/epoch_processing.ex +++ b/lib/lambda_ethereum_consensus/state_transition/epoch_processing.ex @@ -422,8 +422,6 @@ defmodule LambdaEthereumConsensus.StateTransition.EpochProcessing do ) {:ok, new_state} - else - {:error, reason} -> {:error, reason} end end @@ -516,25 +514,27 @@ defmodule LambdaEthereumConsensus.StateTransition.EpochProcessing do if Accessors.get_current_epoch(state) == Constants.genesis_epoch() do {:ok, state} else - flag_deltas = + deltas = Constants.participation_flag_weights() |> Stream.with_index() - |> Enum.map(fn {_, index} -> BeaconState.get_flag_index_deltas(state, index) end) - - deltas = flag_deltas ++ [BeaconState.get_inactivity_penalty_deltas(state)] - - Enum.reduce(deltas, state, fn {rewards, penalties}, state -> - state.validators - |> Stream.with_index() - |> Enum.reduce(state, &apply_reward_and_penalty(&1, &2, rewards, penalties)) - end) - |> then(&{:ok, &1}) + |> Stream.map(fn {weight, index} -> + BeaconState.get_flag_index_deltas(state, weight, index) + end) + |> Stream.concat([BeaconState.get_inactivity_penalty_deltas(state)]) + |> Stream.zip() + + state.balances + |> Stream.zip(deltas) + |> Enum.map(&update_balance/1) + |> then(&{:ok, %{state | balances: &1}}) end end - defp apply_reward_and_penalty({_, index}, state, rewards, penalties) do - state - |> Mutators.increase_balance(index, Enum.at(rewards, index)) - |> BeaconState.decrease_balance(index, Enum.at(penalties, index)) + defp update_balance({balance, deltas}) do + deltas + |> Tuple.to_list() + |> Enum.reduce(balance, fn {reward, penalty}, balance -> + max(balance + reward - penalty, 0) + end) end end diff --git a/lib/lambda_ethereum_consensus/state_transition/misc.ex b/lib/lambda_ethereum_consensus/state_transition/misc.ex index 929478f62..0cade1233 100644 --- a/lib/lambda_ethereum_consensus/state_transition/misc.ex +++ b/lib/lambda_ethereum_consensus/state_transition/misc.ex @@ -102,17 +102,11 @@ defmodule LambdaEthereumConsensus.StateTransition.Misc do @spec decrease_inactivity_score(SszTypes.uint64(), boolean, SszTypes.uint64()) :: SszTypes.uint64() - def decrease_inactivity_score( - inactivity_score, - state_is_in_inactivity_leak, - inactivity_score_recovery_rate - ) do - if state_is_in_inactivity_leak do - inactivity_score - else - inactivity_score - min(inactivity_score_recovery_rate, inactivity_score) - end - end + def decrease_inactivity_score(inactivity_score, true, _inactivity_score_recovery_rate), + do: inactivity_score + + def decrease_inactivity_score(inactivity_score, false, inactivity_score_recovery_rate), + do: inactivity_score - min(inactivity_score_recovery_rate, inactivity_score) @spec update_inactivity_score(%{integer => SszTypes.uint64()}, integer, {SszTypes.uint64()}) :: SszTypes.uint64() diff --git a/lib/lambda_ethereum_consensus/state_transition/operations.ex b/lib/lambda_ethereum_consensus/state_transition/operations.ex index acf8e5a72..f45788fcb 100644 --- a/lib/lambda_ethereum_consensus/state_transition/operations.ex +++ b/lib/lambda_ethereum_consensus/state_transition/operations.ex @@ -3,7 +3,6 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do This module contains functions for handling state transition """ - alias LambdaEthereumConsensus.Engine alias LambdaEthereumConsensus.StateTransition.{Accessors, Misc, Mutators, Predicates} alias LambdaEthereumConsensus.Utils.BitVector alias SszTypes.BeaconBlockBody @@ -242,14 +241,13 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do @doc """ State transition function managing the processing & validation of the `ExecutionPayload` """ - @spec process_execution_payload(BeaconState.t(), ExecutionPayload.t(), boolean()) :: + @spec process_execution_payload(BeaconState.t(), BeaconBlockBody.t(), fun()) :: {:ok, BeaconState.t()} | {:error, String.t()} - - def process_execution_payload(_state, _payload, false) do - {:error, "Invalid execution payload"} - end - - def process_execution_payload(state, payload, _execution_valid) do + def process_execution_payload( + state, + %BeaconBlockBody{execution_payload: payload}, + verify_and_notify_new_payload + ) do cond do # Verify consistency of the parent hash with respect to the previous execution payload header SszTypes.BeaconState.is_merge_transition_complete(state) and @@ -265,7 +263,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do {:error, "Timestamp verification failed"} # Verify the execution payload is valid if not mocked - Engine.Execution.verify_and_notify_new_payload(payload) != {:ok, true} -> + verify_and_notify_new_payload.(payload) != {:ok, true} -> {:error, "Invalid execution payload"} # Cache execution payload header diff --git a/lib/lambda_ethereum_consensus/state_transition/state_transition.ex b/lib/lambda_ethereum_consensus/state_transition/state_transition.ex index 643508e84..81fb02f04 100644 --- a/lib/lambda_ethereum_consensus/state_transition/state_transition.ex +++ b/lib/lambda_ethereum_consensus/state_transition/state_transition.ex @@ -3,6 +3,7 @@ defmodule LambdaEthereumConsensus.StateTransition do State transition logic. """ + alias LambdaEthereumConsensus.Engine.Execution alias LambdaEthereumConsensus.StateTransition alias LambdaEthereumConsensus.StateTransition.{EpochProcessing, Operations} alias SszTypes.{BeaconBlockHeader, BeaconState, SignedBeaconBlock} @@ -48,7 +49,7 @@ defmodule LambdaEthereumConsensus.StateTransition do def process_slots(%BeaconState{slot: old_slot} = state, slot) do slots_per_epoch = ChainSpec.get("SLOTS_PER_EPOCH") - Enum.reduce((old_slot + 1)..slot, {:ok, state}, fn next_slot, acc -> + Enum.reduce((old_slot + 1)..slot//1, {:ok, state}, fn next_slot, acc -> acc |> map(&process_slot/1) # Process epoch on the start slot of the next epoch @@ -57,10 +58,8 @@ defmodule LambdaEthereumConsensus.StateTransition do end) end - defp maybe_process_epoch(%BeaconState{} = state, slot_in_epoch) when slot_in_epoch == 0, - do: {:ok, state} - - defp maybe_process_epoch(%BeaconState{} = state, _slot_in_epoch), do: process_epoch(state) + defp maybe_process_epoch(%BeaconState{} = state, 0), do: process_epoch(state) + defp maybe_process_epoch(%BeaconState{} = state, _slot_in_epoch), do: {:ok, state} defp process_slot(%BeaconState{} = state) do # Cache state root @@ -119,10 +118,12 @@ defmodule LambdaEthereumConsensus.StateTransition do # TODO: uncomment when implemented def process_block(state, block) do + verify_and_notify_new_payload = &Execution.verify_and_notify_new_payload/1 + {:ok, state} |> map(&Operations.process_block_header(&1, block)) |> map(&Operations.process_withdrawals(&1, block.body.execution_payload)) - # |> map(&Operations.process_execution_payload(&1, block.body, EXECUTION_ENGINE)) + |> map(&Operations.process_execution_payload(&1, block.body, verify_and_notify_new_payload)) |> map(&Operations.process_randao(&1, block.body)) |> map(&Operations.process_eth1_data(&1, block.body)) # |> map(&Operations.process_operations(&1, block.body)) diff --git a/lib/spec/runners/operations.ex b/lib/spec/runners/operations.ex index 88e13e58f..cfcf29ab7 100644 --- a/lib/spec/runners/operations.ex +++ b/lib/spec/runners/operations.ex @@ -2,9 +2,12 @@ defmodule OperationsTestRunner do @moduledoc """ Runner for Operations test cases. See: https://github.com/ethereum/consensus-specs/tree/dev/tests/formats/operations """ + alias LambdaEthereumConsensus.StateTransition.Operations alias LambdaEthereumConsensus.Utils.Diff + alias SszTypes.BeaconBlockBody + use ExUnit.CaseTemplate use TestRunner @@ -17,7 +20,7 @@ defmodule OperationsTestRunner do "proposer_slashing" => "ProposerSlashing", "voluntary_exit" => "SignedVoluntaryExit", "sync_aggregate" => "SyncAggregate", - "execution_payload" => "ExecutionPayload", + "execution_payload" => "BeaconBlockBody", "withdrawals" => "ExecutionPayload", "bls_to_execution_change" => "SignedBLSToExecutionChange" # "deposit_receipt" => "DepositReceipt" Not yet implemented @@ -32,7 +35,7 @@ defmodule OperationsTestRunner do "proposer_slashing" => "proposer_slashing", "voluntary_exit" => "voluntary_exit", "sync_aggregate" => "sync_aggregate", - "execution_payload" => "execution_payload", + "execution_payload" => "body", "withdrawals" => "execution_payload", "bls_to_execution_change" => "address_change" # "deposit_receipt" => "deposit_receipt" Not yet implemented @@ -43,19 +46,24 @@ defmodule OperationsTestRunner do @disabled_handlers [ # "attester_slashing", # "attestation", - # "deposit", # "block_header", + # "deposit", # "proposer_slashing", # "voluntary_exit", # "sync_aggregate", - "execution_payload" + # "execution_payload" # "withdrawals", # "bls_to_execution_change" ] @impl TestRunner - def skip?(%SpecTestCase{fork: fork, handler: handler}) do - fork != "capella" or Enum.member?(@disabled_handlers, handler) + def skip?(%SpecTestCase{fork: "capella", handler: handler}) do + Enum.member?(@disabled_handlers, handler) + end + + @impl TestRunner + def skip?(_testcase) do + true end @impl TestRunner @@ -84,19 +92,21 @@ defmodule OperationsTestRunner do handle_case(testcase.handler, pre, operation, post, case_dir) end - defp handle_case("execution_payload", pre, operation, post, case_dir) do + defp handle_case("execution_payload", pre, %BeaconBlockBody{} = body, post, case_dir) do %{execution_valid: execution_valid} = YamlElixir.read_from_file!(case_dir <> "/execution.yaml") |> SpecTestUtils.sanitize_yaml() - new_state = Operations.process_execution_payload(pre, operation, execution_valid) + result = + Operations.process_execution_payload(pre, body, fn _payload -> {:ok, execution_valid} end) case post do nil -> - assert match?({:error, _message}, new_state) + assert {:error, _error_msg} = result - _ -> - assert new_state == {:ok, post} + post -> + assert {:ok, state} = result + assert Diff.diff(state, post) == :unchanged end end diff --git a/lib/ssz_ex.ex b/lib/ssz_ex.ex index 5c201c6d1..85acb55eb 100644 --- a/lib/ssz_ex.ex +++ b/lib/ssz_ex.ex @@ -14,8 +14,14 @@ defmodule LambdaEthereumConsensus.SszEx do else: encode_fixed_size_list(list, basic_type, size) end + def encode(value, {:bytes, _}), do: {:ok, value} + + def encode(container, module) when is_map(container), + do: encode_container(container, module.schema()) + def decode(binary, :bool), do: decode_bool(binary) def decode(binary, {:int, size}), do: decode_uint(binary, size) + def decode(value, {:bytes, _}), do: {:ok, value} def decode(binary, {:list, basic_type, size}) do if variable_size?(basic_type), @@ -23,11 +29,14 @@ defmodule LambdaEthereumConsensus.SszEx do else: decode_list(binary, basic_type, size) end + def decode(binary, module) when is_atom(module), do: decode_container(binary, module) + ################# ### Private functions ################# @bytes_per_length_offset 4 @bits_per_byte 8 + @offset_bits 32 defp encode_int(value, size) when is_integer(value), do: {:ok, <>} defp encode_bool(true), do: {:ok, "\x01"} @@ -184,6 +193,123 @@ defmodule LambdaEthereumConsensus.SszEx do end end + defp encode_container(container, schemas) do + {fixed_size_values, fixed_length, variable_values} = analyze_schemas(container, schemas) + + with {:ok, variable_parts} <- encode_schemas(variable_values), + offsets = calculate_offsets(variable_parts, fixed_length), + variable_length = + Enum.reduce(variable_parts, 0, fn part, acc -> byte_size(part) + acc end), + :ok <- check_length(fixed_length, variable_length), + {:ok, fixed_parts} <- + replace_offsets(fixed_size_values, offsets) + |> encode_schemas do + (fixed_parts ++ variable_parts) + |> Enum.join() + |> then(&{:ok, &1}) + end + end + + defp analyze_schemas(container, schemas) do + schemas + |> Enum.reduce({[], 0, []}, fn {key, schema}, + {acc_fixed_size_values, acc_fixed_length, acc_variable_values} -> + value = Map.fetch!(container, key) + + if variable_size?(schema) do + {[:offset | acc_fixed_size_values], @bytes_per_length_offset + acc_fixed_length, + [{value, schema} | acc_variable_values]} + else + {[{value, schema} | acc_fixed_size_values], acc_fixed_length + get_fixed_size(schema), + acc_variable_values} + end + end) + end + + defp encode_schemas(tuple_values) do + Enum.map(tuple_values, fn {value, schema} -> encode(value, schema) end) + |> flatten_results() + end + + defp calculate_offsets(variable_parts, fixed_length) do + {offsets, _} = + Enum.reduce(variable_parts, {[], fixed_length}, fn element, {res, acc} -> + {[{acc, {:int, 32}} | res], byte_size(element) + acc} + end) + + offsets + end + + defp replace_offsets(fixed_size_values, offsets) do + {fixed_size_values, _} = + Enum.reduce(fixed_size_values, {[], offsets}, &replace_offset/2) + + fixed_size_values + end + + defp replace_offset(:offset, {acc_fixed_list, [offset | rest_offsets]}), + do: {[offset | acc_fixed_list], rest_offsets} + + defp replace_offset(element, {acc_fixed_list, acc_offsets_list}), + do: {[element | acc_fixed_list], acc_offsets_list} + + defp decode_container(binary, module) do + schemas = module.schema() + fixed_length = get_fixed_length(schemas) + <> = binary + + with {:ok, fixed_parts, offsets} <- decode_fixed_section(fixed_binary, schemas, fixed_length), + {:ok, variable_parts} <- decode_variable_section(variable_binary, offsets) do + {:ok, struct!(module, fixed_parts ++ variable_parts)} + end + end + + defp decode_variable_section(binary, offsets) do + offsets + |> Enum.chunk_every(2, 1) + |> Enum.reduce({binary, []}, fn + [{offset, {key, schema}}, {next_offset, _}], {rest_bytes, acc_variable_parts} -> + size = next_offset - offset + <> = rest_bytes + {rest, [{key, decode(chunk, schema)} | acc_variable_parts]} + + [{_offset, {key, schema}}], {rest_bytes, acc_variable_parts} -> + {<<>>, [{key, decode(rest_bytes, schema)} | acc_variable_parts]} + end) + |> then(fn {<<>>, variable_parts} -> + flatten_container_results(variable_parts) + end) + end + + defp decode_fixed_section(binary, schemas, fixed_length) do + schemas + |> Enum.reduce({binary, [], []}, fn {key, schema}, {binary, fixed_parts, offsets} -> + if variable_size?(schema) do + <> = binary + {rest, fixed_parts, [{offset - fixed_length, {key, schema}} | offsets]} + else + ssz_fixed_len = get_fixed_size(schema) + <> = binary + {rest, [{key, decode(chunk, schema)} | fixed_parts], offsets} + end + end) + |> then(fn {_rest_bytes, fixed_parts, offsets} -> + Tuple.append(flatten_container_results(fixed_parts), Enum.reverse(offsets)) + end) + end + + defp get_fixed_length(schemas) do + schemas + |> Stream.map(fn {_key, schema} -> + if variable_size?(schema) do + @bytes_per_length_offset + else + get_fixed_size(schema) + end + end) + |> Enum.sum() + end + # https://notes.ethereum.org/ruKvDXl6QOW3gnqVYb8ezA?view defp sanitize_offset(offset, previous_offset, num_bytes, num_fixed_bytes) do cond do @@ -227,6 +353,15 @@ defmodule LambdaEthereumConsensus.SszEx do end end + defp flatten_container_results(results) do + case Enum.group_by(results, fn {_, {type, _}} -> type end, fn {key, {_, result}} -> + {key, result} + end) do + %{error: errors} -> {:error, errors} + summary -> {:ok, Map.get(summary, :ok, [])} + end + end + defp check_length(fixed_lengths, total_byte_size) do if fixed_lengths + total_byte_size < 2 ** (@bytes_per_length_offset * @bits_per_byte) do @@ -238,8 +373,10 @@ defmodule LambdaEthereumConsensus.SszEx do defp get_fixed_size(:bool), do: 1 defp get_fixed_size({:int, size}), do: div(size, @bits_per_byte) + defp get_fixed_size({:bytes, size}), do: size defp variable_size?({:list, _, _}), do: true defp variable_size?(:bool), do: false defp variable_size?({:int, _}), do: false + defp variable_size?({:bytes, _}), do: false end diff --git a/lib/ssz_types/beacon_chain/beacon_state.ex b/lib/ssz_types/beacon_chain/beacon_state.ex index f71e3d3d5..ad8ab4471 100644 --- a/lib/ssz_types/beacon_chain/beacon_state.ex +++ b/lib/ssz_types/beacon_chain/beacon_state.ex @@ -117,15 +117,14 @@ defmodule SszTypes.BeaconState do @doc """ Return the deltas for a given ``flag_index`` by scanning through the participation flags. """ - @spec get_flag_index_deltas(t(), integer) :: {list(SszTypes.gwei()), list(SszTypes.gwei())} - def get_flag_index_deltas(state, flag_index) do + @spec get_flag_index_deltas(t(), integer(), integer()) :: + Enumerable.t({SszTypes.gwei(), SszTypes.gwei()}) + def get_flag_index_deltas(state, weight, flag_index) do previous_epoch = Accessors.get_previous_epoch(state) {:ok, unslashed_participating_indices} = Accessors.get_unslashed_participating_indices(state, flag_index, previous_epoch) - weight = Enum.at(Constants.participation_flag_weights(), flag_index) - unslashed_participating_balance = Accessors.get_total_balance(state, unslashed_participating_indices) @@ -139,28 +138,37 @@ defmodule SszTypes.BeaconState do weight_denominator = Constants.weight_denominator() - penalties = rewards = List.duplicate(0, length(state.validators)) + previous_epoch = Accessors.get_previous_epoch(state) - Accessors.get_eligible_validator_indices(state) - |> Enum.reduce({rewards, penalties}, fn index, {rewards, penalties} -> + process_reward_and_penalty = fn index -> base_reward = Accessors.get_base_reward(state, index) is_unslashed = MapSet.member?(unslashed_participating_indices, index) cond do is_unslashed and Predicates.is_in_inactivity_leak(state) -> - {rewards, penalties} + {0, 0} is_unslashed -> reward_numerator = base_reward * weight * unslashed_participating_increments reward = div(reward_numerator, active_increments * weight_denominator) - {List.update_at(rewards, index, &(&1 + reward)), penalties} + {reward, 0} flag_index != Constants.timely_head_flag_index() -> penalty = div(base_reward * weight, weight_denominator) - {rewards, List.update_at(penalties, index, &(&1 + penalty))} + {0, penalty} true -> - {rewards, penalties} + {0, 0} + end + end + + state.validators + |> Stream.with_index() + |> Stream.map(fn {validator, index} -> + if Predicates.is_eligible_validator(validator, previous_epoch) do + process_reward_and_penalty.(index) + else + {0, 0} end end) end @@ -169,36 +177,33 @@ defmodule SszTypes.BeaconState do Return the inactivity penalty deltas by considering timely target participation flags and inactivity scores. """ - @spec get_inactivity_penalty_deltas(t()) :: {list(SszTypes.gwei()), list(SszTypes.gwei())} - def get_inactivity_penalty_deltas(state) do - n_validator = length(state.validators) - rewards = List.duplicate(0, n_validator) - penalties = List.duplicate(0, n_validator) + @spec get_inactivity_penalty_deltas(t()) :: Enumerable.t({SszTypes.gwei(), SszTypes.gwei()}) + def get_inactivity_penalty_deltas(%__MODULE__{} = state) do previous_epoch = Accessors.get_previous_epoch(state) - {:ok, unslashed_participating_indices} = + {:ok, matching_target_indices} = Accessors.get_unslashed_participating_indices( state, Constants.timely_target_flag_index(), previous_epoch ) - matching_target_indices = MapSet.new(unslashed_participating_indices) - penalty_denominator = ChainSpec.get("INACTIVITY_SCORE_BIAS") * ChainSpec.get("INACTIVITY_PENALTY_QUOTIENT_BELLATRIX") - state - |> Accessors.get_eligible_validator_indices() - |> Stream.filter(&(not MapSet.member?(matching_target_indices, &1))) - |> Enum.reduce({rewards, penalties}, fn index, {rw, pn} -> - penalty_numerator = - Enum.at(state.validators, index).effective_balance * - Enum.at(state.inactivity_scores, index) - - penalty = div(penalty_numerator, penalty_denominator) - {rw, List.update_at(pn, index, &(&1 + penalty))} + state.validators + |> Stream.zip(state.inactivity_scores) + |> Stream.with_index() + |> Stream.map(fn {{validator, inactivity_score}, index} -> + if Predicates.is_eligible_validator(validator, previous_epoch) and + not MapSet.member?(matching_target_indices, index) do + penalty_numerator = validator.effective_balance * inactivity_score + penalty = div(penalty_numerator, penalty_denominator) + {0, penalty} + else + {0, 0} + end end) end end diff --git a/lib/ssz_types/beacon_chain/sync_committee.ex b/lib/ssz_types/beacon_chain/sync_committee.ex index 64a9b19a5..06e94b12c 100644 --- a/lib/ssz_types/beacon_chain/sync_committee.ex +++ b/lib/ssz_types/beacon_chain/sync_committee.ex @@ -4,6 +4,8 @@ defmodule SszTypes.SyncCommittee do Related definitions in `native/ssz_nif/src/types/`. """ + @behaviour LambdaEthereumConsensus.Container + fields = [ :pubkeys, :aggregate_pubkey @@ -16,4 +18,12 @@ defmodule SszTypes.SyncCommittee do pubkeys: list(SszTypes.bls_pubkey()), aggregate_pubkey: SszTypes.bls_pubkey() } + + @impl LambdaEthereumConsensus.Container + def schema do + [ + {:pubkeys, {:list, {:bytes, 48}, 100}}, + {:aggregate_pubkey, {:bytes, 48}} + ] + end end diff --git a/lib/ssz_types/beacon_chain/validator.ex b/lib/ssz_types/beacon_chain/validator.ex index 615f2100a..e65bf10f4 100644 --- a/lib/ssz_types/beacon_chain/validator.ex +++ b/lib/ssz_types/beacon_chain/validator.ex @@ -3,6 +3,7 @@ defmodule SszTypes.Validator do Struct definition for `Validator`. Related definitions in `native/ssz_nif/src/types/`. """ + @behaviour LambdaEthereumConsensus.Container @eth1_address_withdrawal_prefix <<0x01>> @@ -66,4 +67,18 @@ defmodule SszTypes.Validator do has_excess_balance = balance > max_effective_balance has_eth1_withdrawal_credential(validator) && has_max_effective_balance && has_excess_balance end + + @impl LambdaEthereumConsensus.Container + def schema do + [ + {:pubkey, {:bytes, 48}}, + {:withdrawal_credentials, {:bytes, 32}}, + {:effective_balance, {:int, 64}}, + {:slashed, :bool}, + {:activation_eligibility_epoch, {:int, 64}}, + {:activation_epoch, {:int, 64}}, + {:exit_epoch, {:int, 64}}, + {:withdrawable_epoch, {:int, 64}} + ] + end end diff --git a/native/ssz_nif/src/utils/helpers.rs b/native/ssz_nif/src/utils/helpers.rs index 481c4b801..24fd300ae 100644 --- a/native/ssz_nif/src/utils/helpers.rs +++ b/native/ssz_nif/src/utils/helpers.rs @@ -78,8 +78,9 @@ where Elx: Decoder<'a>, Ssz: TreeHash + FromElx, { + let list_size = list.len(); let root = hash_vector_tree_root::<'a, Elx, Ssz>((list, max_size))?; - let bytes = tree_hash::mix_in_length(&Hash256::from(root), max_size).0; + let bytes = tree_hash::mix_in_length(&Hash256::from(root), list_size).0; Ok(bytes) } @@ -95,16 +96,15 @@ where Ok(vec_tree_hash_root(&x, max_size)) } -/// A helper function providing common functionality between the `TreeHash` implementations for -/// `FixedVector` and `VariableList`. -pub fn vec_tree_hash_root(vec: &[T], size: usize) -> [u8; 32] +/// Taken from `ssz_types` and modified to take `max_size` as dynamic parameter. +pub fn vec_tree_hash_root(vec: &[T], max_size: usize) -> [u8; 32] where T: TreeHash, { let root = match T::tree_hash_type() { TreeHashType::Basic => { - let mut hasher: MerkleHasher = MerkleHasher::with_leaves( - (size + T::tree_hash_packing_factor() - 1) / T::tree_hash_packing_factor(), + let mut hasher = MerkleHasher::with_leaves( + (max_size + T::tree_hash_packing_factor() - 1) / T::tree_hash_packing_factor(), ); for item in vec { @@ -118,7 +118,7 @@ where .expect("ssz_types variable vec should not have a remaining buffer") } TreeHashType::Container | TreeHashType::List | TreeHashType::Vector => { - let mut hasher = MerkleHasher::with_leaves(size); + let mut hasher = MerkleHasher::with_leaves(max_size); for item in vec { hasher diff --git a/test/unit/ssz_ex_test.exs b/test/unit/ssz_ex_test.exs index 766a215b9..3ad2603a5 100644 --- a/test/unit/ssz_ex_test.exs +++ b/test/unit/ssz_ex_test.exs @@ -63,4 +63,74 @@ defmodule Unit.SSZExTest do # empty list assert_roundtrip(<<>>, [], {:list, {:int, 32}, 6}) end + + test "serialize and deserialize container only with fixed parts" do + validator = %SszTypes.Validator{ + pubkey: + <<166, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, + 60, 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, + 160, 206, 88, 144, 58, 231, 142, 94, 235>>, + withdrawal_credentials: + <<31, 83, 167, 245, 158, 202, 157, 114, 98, 134, 215, 52, 106, 152, 108, 188, 15, 122, 21, + 35, 113, 166, 17, 202, 159, 46, 180, 113, 98, 99, 233, 2>>, + effective_balance: 2_281_329_295_298_915_107, + slashed: false, + activation_eligibility_epoch: 8_916_476_893_047_043_501, + activation_epoch: 11_765_006_084_061_081_232, + exit_epoch: 14_221_179_644_044_541_938, + withdrawable_epoch: 11_813_934_873_299_048_632 + } + + serialized = + <<166, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, + 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, + 206, 88, 144, 58, 231, 142, 94, 235, 31, 83, 167, 245, 158, 202, 157, 114, 98, 134, 215, + 52, 106, 152, 108, 188, 15, 122, 21, 35, 113, 166, 17, 202, 159, 46, 180, 113, 98, 99, + 233, 2, 35, 235, 251, 53, 232, 232, 168, 31, 0, 173, 53, 12, 34, 126, 176, 189, 123, 144, + 46, 197, 36, 179, 178, 69, 163, 242, 127, 74, 10, 138, 199, 91, 197, 184, 216, 150, 162, + 44, 135, 243, 163>> + + assert_roundtrip(serialized, validator, SszTypes.Validator) + end + + test "serialize and deserialize variable container" do + pubkey1 = + <<166, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, + 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, + 206, 88, 144, 58, 231, 142, 94, 235>> + + pubkey2 = + <<180, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, + 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, + 206, 88, 144, 58, 231, 142, 94, 235>> + + pubkey3 = + <<190, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, + 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, + 206, 88, 144, 58, 231, 142, 94, 235>> + + pubkey4 = + <<200, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, + 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, + 206, 88, 144, 58, 231, 142, 94, 235>> + + sync = %SszTypes.SyncCommittee{ + pubkeys: [pubkey1, pubkey2, pubkey3], + aggregate_pubkey: pubkey4 + } + + serialized = + <<52, 0, 0, 0, 200, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, + 245, 54, 60, 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, + 203, 11, 160, 206, 88, 144, 58, 231, 142, 94, 235, 166, 144, 240, 158, 185, 117, 206, 31, + 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, 97, 78, 24, 81, 227, 157, 191, 150, + 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, 206, 88, 144, 58, 231, 142, 94, 235, + 180, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, 53, 183, 95, 32, 20, 57, 245, 54, 60, + 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, 46, 131, 80, 54, 55, 203, 11, 160, + 206, 88, 144, 58, 231, 142, 94, 235, 190, 144, 240, 158, 185, 117, 206, 31, 49, 45, 247, + 53, 183, 95, 32, 20, 57, 245, 54, 60, 97, 78, 24, 81, 227, 157, 191, 150, 163, 202, 1, 72, + 46, 131, 80, 54, 55, 203, 11, 160, 206, 88, 144, 58, 231, 142, 94, 235>> + + assert_roundtrip(serialized, sync, SszTypes.SyncCommittee) + end end diff --git a/test/unit/ssz_test.exs b/test/unit/ssz_test.exs index 846ddd09c..3ae214b2d 100644 --- a/test/unit/ssz_test.exs +++ b/test/unit/ssz_test.exs @@ -171,10 +171,17 @@ defmodule Unit.SSZTests do Enum.join(deserialized) ]) + assert serialized == + Base.decode16!( + "0C000000140000001F000000617366617366617331383431383238303139327A6439673861733066373061307366" + ) + assert {:ok, ^serialized} = Ssz.to_ssz_typed(deserialized, SszTypes.Transaction) assert {:ok, ^deserialized} = Ssz.list_from_ssz(serialized, SszTypes.Transaction) - assert {:ok, _hash} = + hash = Base.decode16!("D5ACD42F851C9AE241B55AB79B23D7EC613E01BB6404B4A49D8CF214DBA26CF2") + + assert {:ok, ^hash} = Ssz.hash_list_tree_root_typed(deserialized, 1_048_576, SszTypes.Transaction) end