From fbfe49c2b4679548782b0948902a878de804324b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Avila=20Gast=C3=B3n?= <72628438+avilagaston9@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:29:13 -0300 Subject: [PATCH] fix: retrieve a state by slot (#1231) --- Makefile | 2 +- .../p2p/gossip/beacon_block.ex | 2 +- .../store/state_db.ex | 147 +++++++--------- .../store/state_db/block_root_by_slot.ex | 51 ++++++ .../store/state_db/state_info_by_root.ex | 26 +++ .../state_db/state_root_by_block_root.ex | 24 +++ lib/types/state_info.ex | 16 +- test/unit/beacon_api/beacon_api_v1_test.exs | 4 +- test/unit/store/block_root_by_slot_test.exs | 95 +++++++++++ test/unit/store/state_db_test.exs | 161 ++++++++++++++++++ test/unit/store/state_info_by_root_test.exs | 61 +++++++ .../store/state_root_by_block_root_test.exs | 59 +++++++ 12 files changed, 554 insertions(+), 94 deletions(-) create mode 100644 lib/lambda_ethereum_consensus/store/state_db/block_root_by_slot.ex create mode 100644 lib/lambda_ethereum_consensus/store/state_db/state_info_by_root.ex create mode 100644 lib/lambda_ethereum_consensus/store/state_db/state_root_by_block_root.ex create mode 100644 test/unit/store/block_root_by_slot_test.exs create mode 100644 test/unit/store/state_db_test.exs create mode 100644 test/unit/store/state_info_by_root_test.exs create mode 100644 test/unit/store/state_root_by_block_root_test.exs diff --git a/Makefile b/Makefile index 561060489..a5c9ee815 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all - iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia + iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all diff --git a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex index 51eae17d9..b09efccf7 100644 --- a/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex +++ b/lib/lambda_ethereum_consensus/p2p/gossip/beacon_block.ex @@ -63,7 +63,7 @@ defmodule LambdaEthereumConsensus.P2P.Gossip.BeaconBlock do ### Private functions ########################## - @spec validate(SignedBeaconBlock.t(), Types.slot()) :: :ok | {:ignore, atom()} + @spec validate(SignedBeaconBlock.t(), Types.slot()) :: :ok | {:ignore, String.t()} defp validate(%SignedBeaconBlock{message: block}, current_slot) do min_slot = current_slot - ChainSpec.get("SLOTS_PER_EPOCH") diff --git a/lib/lambda_ethereum_consensus/store/state_db.ex b/lib/lambda_ethereum_consensus/store/state_db.ex index 4c054ec77..e13cab075 100644 --- a/lib/lambda_ethereum_consensus/store/state_db.ex +++ b/lib/lambda_ethereum_consensus/store/state_db.ex @@ -1,119 +1,100 @@ defmodule LambdaEthereumConsensus.Store.StateDb do @moduledoc """ - Beacon node state storage. + This module offers an interface to manage beacon node state storage. + + The module coordinates the interaction with the following key-value stores: + * `StateInfoByRoot` - Maps state roots to states. + * `StateRootByBlockRoot` - Maps block roots to state roots. + * `BlockRootBySlot` - Maps slots to block roots. """ require Logger - alias LambdaEthereumConsensus.Store.Db - alias LambdaEthereumConsensus.Store.Utils + alias LambdaEthereumConsensus.Store.StateDb.BlockRootBySlot + alias LambdaEthereumConsensus.Store.StateDb.StateInfoByRoot + alias LambdaEthereumConsensus.Store.StateDb.StateRootByBlockRoot alias Types.BeaconState alias Types.StateInfo - @state_prefix "beacon_state" - @state_block_prefix "beacon_state_by_state" - @stateslot_prefix @state_prefix <> "slot" + ########################## + ### Public API + ########################## @spec store_state_info(StateInfo.t()) :: :ok def store_state_info(%StateInfo{} = state_info) do - key_block = state_key(state_info.block_root) - key_state = block_key(state_info.root) - Db.put(key_block, StateInfo.encode(state_info)) - Db.put(key_state, state_info.root) - + StateInfoByRoot.put(state_info.root, state_info) + StateRootByBlockRoot.put(state_info.block_root, state_info.root) # WARN: this overrides any previous mapping for the same slot - slothash_key_block = root_by_slot_key(state_info.beacon_state.slot) - Db.put(slothash_key_block, state_info.root) - end - - @spec prune_states_older_than(non_neg_integer()) :: :ok | {:error, String.t()} | :not_found - def prune_states_older_than(slot) do - Logger.info("[StateDb] Pruning started.", slot: slot) - last_finalized_key = slot |> root_by_slot_key() - - with {:ok, it} <- Db.iterate(), - {:ok, @stateslot_prefix <> _slot, _value} <- - Exleveldb.iterator_move(it, last_finalized_key), - {:ok, slots_to_remove} <- get_slots_to_remove(it), - :ok <- Exleveldb.iterator_close(it) do - slots_to_remove |> Enum.each(&remove_state_by_slot/1) - Logger.info("[StateDb] Pruning finished. #{length(slots_to_remove)} states removed.") - end - end - - @spec get_slots_to_remove(list(non_neg_integer()), :eleveldb.itr_ref()) :: - {:ok, list(non_neg_integer())} - defp get_slots_to_remove(slots_to_remove \\ [], iterator) do - case Exleveldb.iterator_move(iterator, :prev) do - {:ok, @stateslot_prefix <> <>, _root} -> - [slot | slots_to_remove] |> get_slots_to_remove(iterator) - - _ -> - {:ok, slots_to_remove} - end - end - - @spec remove_state_by_slot(non_neg_integer()) :: :ok | :not_found - defp remove_state_by_slot(slot) do - key_slot = root_by_slot_key(slot) - - with {:ok, block_root} <- Db.get(key_slot), - key_block <- state_key(block_root), - {:ok, encoded_state} <- Db.get(key_block), - {:ok, state_info} <- StateInfo.decode(encoded_state, block_root) do - key_state = block_key(state_info.root) - - Db.delete(key_slot) - Db.delete(key_block) - Db.delete(key_state) - end + BlockRootBySlot.put(state_info.beacon_state.slot, state_info.block_root) end @spec get_state_by_block_root(Types.root()) :: {:ok, StateInfo.t()} | {:error, String.t()} | :not_found def get_state_by_block_root(block_root) do - with {:ok, bin} <- block_root |> state_key() |> Db.get() do - StateInfo.decode(bin, block_root) + with {:ok, state_root} <- StateRootByBlockRoot.get(block_root) do + StateInfoByRoot.get(state_root) end end @spec get_state_by_state_root(Types.root()) :: {:ok, StateInfo.t()} | {:error, String.t()} | :not_found - def get_state_by_state_root(state_root) do - with {:ok, block_root} <- state_root |> block_key() |> Db.get() do - get_state_by_block_root(block_root) - end - end + def get_state_by_state_root(state_root), do: StateInfoByRoot.get(state_root) @spec get_latest_state() :: {:ok, StateInfo.t()} | {:error, String.t()} | :not_found def get_latest_state() do - last_key = root_by_slot_key(0xFFFFFFFFFFFFFFFF) - - with {:ok, it} <- Db.iterate(), - {:ok, _key, _value} <- Exleveldb.iterator_move(it, last_key), - {:ok, @stateslot_prefix <> _slot, root} <- Exleveldb.iterator_move(it, :prev), - :ok <- Exleveldb.iterator_close(it) do - get_state_by_block_root(root) - else - {:ok, _key, _value} -> :not_found - {:error, :invalid_iterator} -> :not_found + with {:ok, last_block_root} <- BlockRootBySlot.get_last_slot_block_root(), + {:ok, last_state_root} <- StateRootByBlockRoot.get(last_block_root) do + StateInfoByRoot.get(last_state_root) end end - @spec get_state_root_by_slot(Types.slot()) :: - {:ok, Types.root()} | {:error, String.t()} | :not_found - def get_state_root_by_slot(slot), - do: slot |> root_by_slot_key() |> Db.get() - @spec get_state_by_slot(Types.slot()) :: {:ok, BeaconState.t()} | {:error, String.t()} | :not_found def get_state_by_slot(slot) do # WARN: this will return the latest state received for the given slot - with {:ok, root} <- get_state_root_by_slot(slot) do - get_state_by_block_root(root) + with {:ok, block_root} <- BlockRootBySlot.get(slot) do + get_state_by_block_root(block_root) end end - defp state_key(root), do: Utils.get_key(@state_prefix, root) - defp block_key(root), do: Utils.get_key(@state_block_prefix, root) - defp root_by_slot_key(slot), do: Utils.get_key(@stateslot_prefix, slot) + @spec prune_states_older_than(non_neg_integer()) :: :ok | {:error, String.t()} | :not_found + def prune_states_older_than(slot) do + Logger.info("[StateDb] Pruning started.", slot: slot) + + result = + BlockRootBySlot.fold_keys(slot, 0, fn slot, acc -> + case BlockRootBySlot.get(slot) do + {:ok, _block_root} -> + remove_state_by_slot(slot) + acc + 1 + + other -> + Logger.error( + "[Block pruning] Failed to remove block from slot #{inspect(slot)}. Reason: #{inspect(other)}" + ) + end + end) + + # TODO: the separate get operation is avoided if we implement folding with values in KvSchema. + case result do + {:ok, n_removed} -> + Logger.info("[StateDb] Pruning finished. #{inspect(n_removed)} states removed.") + + {:error, reason} -> + Logger.error("[StateDb] Error pruning states: #{inspect(reason)}") + end + end + + ########################## + ### Private Functions + ########################## + + @spec remove_state_by_slot(non_neg_integer()) :: :ok | :not_found + defp remove_state_by_slot(slot) do + with {:ok, block_root} <- BlockRootBySlot.get(slot), + {:ok, state_root} <- StateRootByBlockRoot.get(block_root) do + BlockRootBySlot.delete(slot) + StateRootByBlockRoot.delete(block_root) + StateInfoByRoot.delete(state_root) + end + end end diff --git a/lib/lambda_ethereum_consensus/store/state_db/block_root_by_slot.ex b/lib/lambda_ethereum_consensus/store/state_db/block_root_by_slot.ex new file mode 100644 index 000000000..4a78d853a --- /dev/null +++ b/lib/lambda_ethereum_consensus/store/state_db/block_root_by_slot.ex @@ -0,0 +1,51 @@ +defmodule LambdaEthereumConsensus.Store.StateDb.BlockRootBySlot do + @moduledoc """ + KvSchema that stores block roots indexed by slots. + """ + alias LambdaEthereumConsensus.Store.KvSchema + require Logger + use KvSchema, prefix: "statedb_block_root_by_slot" + + @impl KvSchema + @spec encode_key(Types.slot()) :: {:ok, binary()} | {:error, binary()} + def encode_key(slot), do: {:ok, <>} + + @impl KvSchema + @spec decode_key(binary()) :: {:ok, integer()} | {:error, binary()} + def decode_key(<>), do: {:ok, slot} + + def decode_key(other) do + {:error, "[Block by slot] Could not decode slot, not 64 bit integer: #{other}"} + end + + @impl KvSchema + @spec encode_value(Types.root()) :: {:ok, Types.root()} | {:error, binary()} + def encode_value(<<_::256>> = root), do: {:ok, root} + + @impl KvSchema + @spec decode_value(Types.root()) :: {:ok, Types.root()} | {:error, binary()} + def decode_value(<<_::256>> = root), do: {:ok, root} + + @spec get_last_slot_block_root() :: {:ok, Types.root()} | :not_found + def get_last_slot_block_root() do + with {:ok, first_slot} <- first_key() do + fold_keys( + first_slot, + nil, + fn slot, _acc -> + case get(slot) do + {:ok, block_root} -> + block_root + + other -> + Logger.error( + "[Block pruning] Failed to find last slot root #{inspect(slot)}. Reason: #{inspect(other)}" + ) + end + end, + direction: :next, + include_first: true + ) + end + end +end diff --git a/lib/lambda_ethereum_consensus/store/state_db/state_info_by_root.ex b/lib/lambda_ethereum_consensus/store/state_db/state_info_by_root.ex new file mode 100644 index 000000000..4a1d825e4 --- /dev/null +++ b/lib/lambda_ethereum_consensus/store/state_db/state_info_by_root.ex @@ -0,0 +1,26 @@ +defmodule LambdaEthereumConsensus.Store.StateDb.StateInfoByRoot do + @moduledoc """ + KvSchema that stores states indexed by their roots. + """ + + alias LambdaEthereumConsensus.Store.KvSchema + alias Types.StateInfo + use KvSchema, prefix: "statedb_state_by_root" + + @impl KvSchema + @spec encode_key(Types.root()) :: {:ok, binary()} + def encode_key(root) when is_binary(root), do: {:ok, root} + + @impl KvSchema + @spec decode_key(binary()) :: {:ok, Types.root()} + def decode_key(root) when is_binary(root), do: {:ok, root} + + @impl KvSchema + @spec encode_value(StateInfo.t()) :: {:ok, binary()} | {:error, binary()} + def encode_value(%StateInfo{} = state_info), do: {:ok, StateInfo.encode(state_info)} + + @impl KvSchema + @spec decode_value(binary()) :: {:ok, StateInfo.t()} | {:error, binary()} + def decode_value(encoded_state) when is_binary(encoded_state), + do: StateInfo.decode(encoded_state) +end diff --git a/lib/lambda_ethereum_consensus/store/state_db/state_root_by_block_root.ex b/lib/lambda_ethereum_consensus/store/state_db/state_root_by_block_root.ex new file mode 100644 index 000000000..2ad6ea2c6 --- /dev/null +++ b/lib/lambda_ethereum_consensus/store/state_db/state_root_by_block_root.ex @@ -0,0 +1,24 @@ +defmodule LambdaEthereumConsensus.Store.StateDb.StateRootByBlockRoot do + @moduledoc """ + KvSchema that stores state roots indexed by BeaconBlock roots. + """ + + alias LambdaEthereumConsensus.Store.KvSchema + use KvSchema, prefix: "statedb_state_root_by_block_root" + + @impl KvSchema + @spec encode_key(Types.root()) :: {:ok, binary()} + def encode_key(<<_::256>> = root), do: {:ok, root} + + @impl KvSchema + @spec decode_key(Types.root()) :: {:ok, Types.root()} + def decode_key(<<_::256>> = root), do: {:ok, root} + + @impl KvSchema + @spec encode_value(Types.root()) :: {:ok, binary()} + def encode_value(<<_::256>> = root), do: {:ok, root} + + @impl KvSchema + @spec decode_value(Types.root()) :: {:ok, Types.root()} | {:error, binary()} + def decode_value(<<_::256>> = root), do: {:ok, root} +end diff --git a/lib/types/state_info.ex b/lib/types/state_info.ex index 141cd97f6..2e422053c 100644 --- a/lib/types/state_info.ex +++ b/lib/types/state_info.ex @@ -5,6 +5,7 @@ defmodule Types.StateInfo do - root: The hash tree root of the state, so that we don't recalculate it before saving. - encoded: The ssz encoded version of the state. It's common that we save a state after + Warning: Do not modify this manually. If you do, you may need to re-encode the beacon state using `from_beacon_state`. """ alias Types.BeaconState @@ -37,12 +38,12 @@ defmodule Types.StateInfo do @spec encode(t()) :: binary() def encode(%__MODULE__{} = state_info) do - {state_info.encoded, state_info.root} |> :erlang.term_to_binary() + {state_info.encoded, state_info.root, state_info.block_root} |> :erlang.term_to_binary() end - @spec decode(binary(), Types.root()) :: {:ok, t()} | {:error, binary()} - def decode(bin, block_root) do - with {:ok, encoded, root} <- :erlang.binary_to_term(bin) |> validate_term(), + @spec decode(binary()) :: {:ok, t()} | {:error, binary()} + def decode(bin) do + with {:ok, encoded, root, block_root} <- :erlang.binary_to_term(bin) |> validate_term(), {:ok, beacon_state} <- Ssz.from_ssz(encoded, BeaconState) do {:ok, %__MODULE__{ @@ -58,9 +59,10 @@ defmodule Types.StateInfo do with :error <- Keyword.fetch(keyword, key), do: fun.() end - @spec validate_term(term()) :: {:ok, binary(), Types.root()} | {:error, binary()} - defp validate_term({ssz_encoded, root}) when is_binary(ssz_encoded) and is_binary(root) do - {:ok, ssz_encoded, root} + @spec validate_term(term()) :: {:ok, binary(), Types.root(), Types.root()} | {:error, binary()} + defp validate_term({ssz_encoded, root, block_root}) + when is_binary(ssz_encoded) and is_binary(root) and is_binary(root) do + {:ok, ssz_encoded, root, block_root} end defp validate_term(other) do diff --git a/test/unit/beacon_api/beacon_api_v1_test.exs b/test/unit/beacon_api/beacon_api_v1_test.exs index eefe28d02..0157fdac8 100644 --- a/test/unit/beacon_api/beacon_api_v1_test.exs +++ b/test/unit/beacon_api/beacon_api_v1_test.exs @@ -96,8 +96,8 @@ defmodule Unit.BeaconApiTest.V1 do beacon_state = Fixtures.Block.beacon_state() patch( - LambdaEthereumConsensus.Store.StateDb, - :get_state_by_state_root, + LambdaEthereumConsensus.Store.StateDb.StateInfoByRoot, + :get, {:ok, beacon_state} ) diff --git a/test/unit/store/block_root_by_slot_test.exs b/test/unit/store/block_root_by_slot_test.exs new file mode 100644 index 000000000..cd1177e3e --- /dev/null +++ b/test/unit/store/block_root_by_slot_test.exs @@ -0,0 +1,95 @@ +defmodule Unit.Store.BlockRootBySlotTest do + alias Fixtures.Random + alias LambdaEthereumConsensus.Store.StateDb.BlockRootBySlot + + use ExUnit.Case + + setup %{tmp_dir: tmp_dir} do + start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) + :ok + end + + @tag :tmp_dir + test "Get on a non-existent slot" do + slot = Random.slot() + assert :not_found == BlockRootBySlot.get(slot) + end + + @tag :tmp_dir + test "Basic block root saving" do + root = Random.root() + slot = Random.slot() + assert :ok == BlockRootBySlot.put(slot, root) + assert {:ok, root} == BlockRootBySlot.get(slot) + end + + @tag :tmp_dir + test "Basic saving of two block roots" do + root1 = Random.root() + slot1 = Random.slot() + root2 = Random.root() + slot2 = Random.slot() + + assert :ok == BlockRootBySlot.put(slot1, root1) + assert :ok == BlockRootBySlot.put(slot2, root2) + + assert {:ok, root1} == BlockRootBySlot.get(slot1) + assert {:ok, root2} == BlockRootBySlot.get(slot2) + end + + @tag :tmp_dir + test "Delete a single slot" do + root1 = Random.root() + slot1 = Random.slot() + root2 = Random.root() + slot2 = Random.slot() + + assert :ok == BlockRootBySlot.put(slot1, root1) + assert :ok == BlockRootBySlot.put(slot2, root2) + assert :ok == BlockRootBySlot.delete(slot2) + + assert {:ok, root1} == BlockRootBySlot.get(slot1) + assert :not_found == BlockRootBySlot.get(slot2) + end + + @tag :tmp_dir + test "Get the root of the last slot" do + [root1, root2, root3] = + [Random.root(), Random.root(), Random.root()] + + [slot1, slot2, slot3] = + [Random.slot(), Random.slot(), Random.slot()] + |> Enum.sort() + + assert :ok == BlockRootBySlot.put(slot1, root1) + assert :ok == BlockRootBySlot.put(slot2, root2) + assert :ok == BlockRootBySlot.put(slot3, root3) + + # Check that the keys are present + assert {:ok, root1} == BlockRootBySlot.get(slot1) + assert {:ok, root2} == BlockRootBySlot.get(slot2) + assert {:ok, root3} == BlockRootBySlot.get(slot3) + + assert {:ok, root3} == BlockRootBySlot.get_last_slot_block_root() + end + + @tag :tmp_dir + test "Get last block root when empty" do + assert :not_found == BlockRootBySlot.get_last_slot_block_root() + end + + @tag :tmp_dir + test "Get last block root with one element" do + root = Random.root() + slot = Random.slot() + + assert :ok == BlockRootBySlot.put(slot, root) + + assert {:ok, root} == BlockRootBySlot.get_last_slot_block_root() + end + + @tag :tmp_dir + test "Attempt to save a non-root binary fails" do + assert_raise(FunctionClauseError, fn -> BlockRootBySlot.put(1, "Hello") end) + end +end diff --git a/test/unit/store/state_db_test.exs b/test/unit/store/state_db_test.exs new file mode 100644 index 000000000..45fd85029 --- /dev/null +++ b/test/unit/store/state_db_test.exs @@ -0,0 +1,161 @@ +defmodule Unit.Store.StateDb do + alias Fixtures.Random + alias LambdaEthereumConsensus.Store.StateDb + alias Types.BeaconState + alias Types.StateInfo + + use ExUnit.Case + + setup_all do + Application.fetch_env!(:lambda_ethereum_consensus, ChainSpec) + |> Keyword.put(:config, MinimalConfig) + |> then(&Application.put_env(:lambda_ethereum_consensus, ChainSpec, &1)) + end + + setup %{tmp_dir: tmp_dir} do + start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) + :ok + end + + defp get_state_info() do + {:ok, encoded} = + File.read!("test/fixtures/validator/proposer/beacon_state.ssz_snappy") + |> :snappyer.decompress() + + {:ok, decoded} = SszEx.decode(encoded, BeaconState) + {:ok, state_info} = StateInfo.from_beacon_state(decoded) + state_info + end + + # Returns a new `state_info` with: + # - `slot` incremented by 1. + # - A random `block_root`. + # - A random `state_root`. + defp modify_state_info(state_info) do + new_beacon_state = state_info.beacon_state |> Map.put(:slot, state_info.beacon_state.slot + 1) + {:ok, new_state_info} = StateInfo.from_beacon_state(new_beacon_state) + + new_state_info + |> Map.put(:block_root, Random.root()) + end + + defp assert_state_is_present(state) do + assert {:ok, state} == StateDb.get_state_by_block_root(state.block_root) + assert {:ok, state} == StateDb.get_state_by_state_root(state.root) + assert {:ok, state} == StateDb.get_state_by_slot(state.beacon_state.slot) + end + + defp assert_state_not_found(state) do + assert :not_found == StateDb.get_state_by_block_root(state.block_root) + assert :not_found == StateDb.get_state_by_state_root(state.root) + assert :not_found == StateDb.get_state_by_slot(state.beacon_state.slot) + end + + @tag :tmp_dir + test "Get on a non-existent block root" do + root = Random.root() + assert :not_found == StateDb.get_state_by_block_root(root) + end + + @tag :tmp_dir + test "Get on a non-existent state root" do + root = Random.root() + assert :not_found == StateDb.get_state_by_state_root(root) + end + + @tag :tmp_dir + test "Get on a non-existent slot" do + slot = Random.slot() + assert :not_found == StateDb.get_state_by_slot(slot) + end + + @tag :tmp_dir + test "Basic saving a state" do + state = get_state_info() + + assert :ok == StateDb.store_state_info(state) + + assert_state_is_present(state) + end + + @tag :tmp_dir + test "Basic saving of two states" do + state1 = get_state_info() + state2 = modify_state_info(state1) + + assert :ok == StateDb.store_state_info(state1) + assert :ok == StateDb.store_state_info(state2) + + assert_state_is_present(state1) + assert_state_is_present(state2) + end + + @tag :tmp_dir + test "Pruning from the first slot" do + state1 = get_state_info() + state2 = modify_state_info(state1) + state3 = modify_state_info(state2) + + assert :ok == StateDb.store_state_info(state1) + assert :ok == StateDb.store_state_info(state2) + assert :ok == StateDb.store_state_info(state3) + + assert_state_is_present(state1) + assert_state_is_present(state2) + assert_state_is_present(state3) + + assert :ok == StateDb.prune_states_older_than(state1.beacon_state.slot) + + assert_state_is_present(state1) + assert_state_is_present(state2) + assert_state_is_present(state3) + end + + @tag :tmp_dir + test "Pruning from the last slot" do + state1 = get_state_info() + state2 = modify_state_info(state1) + state3 = modify_state_info(state2) + + assert :ok == StateDb.store_state_info(state1) + assert :ok == StateDb.store_state_info(state2) + assert :ok == StateDb.store_state_info(state3) + + assert_state_is_present(state1) + assert_state_is_present(state2) + assert_state_is_present(state3) + + assert :ok == StateDb.prune_states_older_than(state3.beacon_state.slot) + + assert_state_not_found(state1) + assert_state_not_found(state2) + assert_state_is_present(state3) + end + + @tag :tmp_dir + test "Get latest state when empty" do + assert :not_found == StateDb.get_latest_state() + end + + @tag :tmp_dir + test "Get latest state with one state" do + state = get_state_info() + + assert :ok == StateDb.store_state_info(state) + + assert {:ok, state} == StateDb.get_latest_state() + end + + @tag :tmp_dir + test "Get latest state with many states" do + state1 = get_state_info() + state2 = modify_state_info(state1) + state3 = modify_state_info(state2) + + assert :ok == StateDb.store_state_info(state1) + assert :ok == StateDb.store_state_info(state2) + assert :ok == StateDb.store_state_info(state3) + + assert {:ok, state3} == StateDb.get_latest_state() + end +end diff --git a/test/unit/store/state_info_by_root_test.exs b/test/unit/store/state_info_by_root_test.exs new file mode 100644 index 000000000..cb7845145 --- /dev/null +++ b/test/unit/store/state_info_by_root_test.exs @@ -0,0 +1,61 @@ +defmodule Unit.Store.StateInfoByRoot do + alias Fixtures.Random + alias LambdaEthereumConsensus.Store.StateDb.StateInfoByRoot + alias Types.BeaconState + alias Types.StateInfo + + use ExUnit.Case + + setup_all do + Application.fetch_env!(:lambda_ethereum_consensus, ChainSpec) + |> Keyword.put(:config, MinimalConfig) + |> then(&Application.put_env(:lambda_ethereum_consensus, ChainSpec, &1)) + end + + setup %{tmp_dir: tmp_dir} do + start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) + :ok + end + + defp get_state_info() do + {:ok, encoded} = + File.read!("test/fixtures/validator/proposer/beacon_state.ssz_snappy") + |> :snappyer.decompress() + + {:ok, decoded} = SszEx.decode(encoded, BeaconState) + {:ok, state_info} = StateInfo.from_beacon_state(decoded) + state_info + end + + @tag :tmp_dir + test "Get on a non-existent root" do + root = Random.root() + assert :not_found == StateInfoByRoot.get(root) + end + + @tag :tmp_dir + test "Basic saving a state" do + state = get_state_info() + assert :ok == StateInfoByRoot.put(state.root, state) + assert {:ok, state} == StateInfoByRoot.get(state.root) + end + + @tag :tmp_dir + test "Delete one state" do + state = get_state_info() + state_root1 = Random.root() + state_root2 = Random.root() + + assert :ok == StateInfoByRoot.put(state_root1, state) + assert :ok == StateInfoByRoot.put(state_root2, state) + assert :ok == StateInfoByRoot.delete(state_root2) + + assert {:ok, state} == StateInfoByRoot.get(state_root1) + assert :not_found == StateInfoByRoot.get(state_root2) + end + + @tag :tmp_dir + test "Trying to save a different type fails" do + assert_raise(FunctionClauseError, fn -> StateInfoByRoot.put(1, "Hello") end) + end +end diff --git a/test/unit/store/state_root_by_block_root_test.exs b/test/unit/store/state_root_by_block_root_test.exs new file mode 100644 index 000000000..8d67e524e --- /dev/null +++ b/test/unit/store/state_root_by_block_root_test.exs @@ -0,0 +1,59 @@ +defmodule Unit.Store.StateRootByBlockRoot do + alias Fixtures.Random + alias LambdaEthereumConsensus.Store.StateDb.StateRootByBlockRoot + + use ExUnit.Case + + setup %{tmp_dir: tmp_dir} do + start_link_supervised!({LambdaEthereumConsensus.Store.Db, dir: tmp_dir}) + :ok + end + + @tag :tmp_dir + test "Get on a non-existent root" do + root = Random.root() + assert :not_found == StateRootByBlockRoot.get(root) + end + + @tag :tmp_dir + test "Basic saving a state root" do + block_root = Random.root() + state_root = Random.root() + assert :ok == StateRootByBlockRoot.put(block_root, state_root) + assert {:ok, state_root} == StateRootByBlockRoot.get(block_root) + end + + @tag :tmp_dir + test "Basic saving of two state roots" do + state_root1 = Random.root() + block_root1 = Random.root() + state_root2 = Random.root() + block_root2 = Random.root() + + assert :ok == StateRootByBlockRoot.put(block_root1, state_root1) + assert :ok == StateRootByBlockRoot.put(block_root2, state_root2) + + assert {:ok, state_root1} == StateRootByBlockRoot.get(block_root1) + assert {:ok, state_root2} == StateRootByBlockRoot.get(block_root2) + end + + @tag :tmp_dir + test "Delete one state root" do + state_root1 = Random.root() + block_root1 = Random.root() + state_root2 = Random.root() + block_root2 = Random.root() + + assert :ok == StateRootByBlockRoot.put(block_root1, state_root1) + assert :ok == StateRootByBlockRoot.put(block_root2, state_root2) + assert :ok == StateRootByBlockRoot.delete(block_root2) + + assert {:ok, state_root1} == StateRootByBlockRoot.get(block_root1) + assert :not_found == StateRootByBlockRoot.get(block_root2) + end + + @tag :tmp_dir + test "Trying to save a non-root binary fails" do + assert_raise(FunctionClauseError, fn -> StateRootByBlockRoot.put(1, "Hello") end) + end +end