From b87159e98f1ef969fadc35a40933d3f19c7e74ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Fri, 19 Apr 2024 20:23:30 +0100 Subject: [PATCH] feat: allow genesis from file for local testnets (#946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com> --- config/runtime.exs | 54 +++--- lib/chain_spec/configs/custom.ex | 28 ++- lib/chain_spec/configs/gen_config.ex | 8 + lib/chain_spec/utils.ex | 2 +- lib/lambda_ethereum_consensus/application.ex | 7 +- .../beacon/beacon_node.ex | 101 +---------- .../beacon/store_setup.ex | 161 ++++++++++++++++++ 7 files changed, 232 insertions(+), 129 deletions(-) create mode 100644 lib/lambda_ethereum_consensus/beacon/store_setup.ex diff --git a/config/runtime.exs b/config/runtime.exs index 00b02b89c..885a374be 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,4 +1,8 @@ import Config +alias LambdaEthereumConsensus.Beacon.StoreSetup +alias LambdaEthereumConsensus.ForkChoice +alias LambdaEthereumConsensus.SszEx +alias Types.BeaconStateDeneb switches = [ network: :string, @@ -41,8 +45,10 @@ validator_file = Keyword.get(args, :validator_file) enable_beacon_api = Keyword.get(args, :beacon_api, false) beacon_api_port = Keyword.get(args, :beacon_api_port, 4000) -config :lambda_ethereum_consensus, LambdaEthereumConsensus.ForkChoice, - checkpoint_sync_url: checkpoint_sync_url +if not is_nil(testnet_dir) and not is_nil(checkpoint_sync_url) do + IO.puts("Both checkpoint sync and testnet url specified (only one should be specified).") + System.halt(2) +end valid_modes = ["full", "db"] raw_mode = Keyword.get(args, :mode, "full") @@ -57,43 +63,49 @@ mode = config :lambda_ethereum_consensus, LambdaEthereumConsensus, mode: mode -datadir = Keyword.get(args, :datadir, "level_db/#{network}") +# DB setup +default_datadir = + case testnet_dir do + nil -> "level_db/#{network}" + _ -> "level_db/local_testnet" + end + +datadir = Keyword.get(args, :datadir, default_datadir) File.mkdir_p!(datadir) config :lambda_ethereum_consensus, LambdaEthereumConsensus.Store.Db, dir: datadir -chain_config = +# Network setup +{chain_config, bootnodes} = case testnet_dir do nil -> config = ConfigUtils.parse_config(network) bootnodes = YamlElixir.read_from_file!("config/networks/#{network}/boot_enr.yaml") - - %{ - config: config, - genesis_validators_root: config.genesis_validators_root(), - bootnodes: bootnodes - } + {config, bootnodes} testnet_dir -> Path.join(testnet_dir, "config.yaml") |> CustomConfig.load_from_file!() + bootnodes = Path.join(testnet_dir, "boot_enr.yaml") |> YamlElixir.read_from_file!() + {CustomConfig, bootnodes} + end - # TODO: compute this from the genesis block - genesis_validators_root = <<0::256>> +# We use put_env here as we need this immediately after to read the state. +Application.put_env(:lambda_ethereum_consensus, ChainSpec, config: chain_config) - bootnodes = Path.join(testnet_dir, "boot_enr.yaml") |> YamlElixir.read_from_file!() +strategy = StoreSetup.make_strategy!(testnet_dir, checkpoint_sync_url) - %{ - config: CustomConfig, - genesis_validators_root: genesis_validators_root, - bootnodes: bootnodes - } +genesis_validators_root = + case strategy do + {:file, state} -> state.genesis_validators_root + _ -> chain_config.genesis_validators_root() end config :lambda_ethereum_consensus, ChainSpec, - config: Map.fetch!(chain_config, :config), - genesis_validators_root: Map.fetch!(chain_config, :genesis_validators_root) + config: chain_config, + genesis_validators_root: genesis_validators_root + +config :lambda_ethereum_consensus, StoreSetup, strategy: strategy # Configures peer discovery -bootnodes = Map.fetch!(chain_config, :bootnodes) config :lambda_ethereum_consensus, :discovery, port: 9000, bootnodes: bootnodes # Engine API diff --git a/lib/chain_spec/configs/custom.ex b/lib/chain_spec/configs/custom.ex index 35ce42f87..75c9a5070 100644 --- a/lib/chain_spec/configs/custom.ex +++ b/lib/chain_spec/configs/custom.ex @@ -8,13 +8,33 @@ defmodule CustomConfig do def load_from_file!(path) do config = ConfigUtils.load_config_from_file!(path) preset = Map.fetch!(config, "PRESET_BASE") |> ConfigUtils.parse_preset() - merged_config = Map.merge(preset.get_preset(), config) + base_config = Map.fetch!(config, "CONFIG_NAME") |> ConfigUtils.parse_config() + + merged_config = + preset.get_preset() + |> Map.merge(base_config.get_all()) + |> Map.merge(config) + Application.put_env(:lambda_ethereum_consensus, __MODULE__, merged: merged_config) end - defp get_config, - do: Application.get_env(:lambda_ethereum_consensus, __MODULE__) |> Keyword.fetch!(:merged) + @impl GenConfig + def get_all do + Application.get_env(:lambda_ethereum_consensus, __MODULE__) + |> Keyword.fetch!(:merged) + |> Map.new(fn {k, v} -> {k, parse_int(v)} end) + end @impl GenConfig - def get(key), do: get_config() |> Map.fetch!(key) + def get(key), do: get_all() |> Map.fetch!(key) + + # Parses as integer if parsable. If not, returns original value. + defp parse_int(v) when is_binary(v) do + case Integer.parse(v) do + {i, ""} -> i + _ -> v + end + end + + defp parse_int(v), do: v end diff --git a/lib/chain_spec/configs/gen_config.ex b/lib/chain_spec/configs/gen_config.ex index 01e831c29..e1459defd 100644 --- a/lib/chain_spec/configs/gen_config.ex +++ b/lib/chain_spec/configs/gen_config.ex @@ -21,6 +21,9 @@ defmodule ChainSpec.GenConfig do @impl unquote(__MODULE__) def get(key), do: Map.fetch!(@__unified, key) + + @impl unquote(__MODULE__) + def get_all, do: @__unified end end @@ -28,4 +31,9 @@ defmodule ChainSpec.GenConfig do Fetches a value from config. """ @callback get(String.t()) :: term() + + @doc """ + Fetches the full config dictionary. + """ + @callback get_all() :: map() end diff --git a/lib/chain_spec/utils.ex b/lib/chain_spec/utils.ex index 488649778..d7df82933 100644 --- a/lib/chain_spec/utils.ex +++ b/lib/chain_spec/utils.ex @@ -7,7 +7,7 @@ defmodule ConfigUtils do def load_config_from_file!(path) do path |> File.read!() - |> String.replace(~r/(0x[0-9a-fA-F]+)/, "'\\g{1}'") + |> String.replace(~r/ (0x[0-9a-fA-F]+)/, " '\\g{1}'") |> YamlElixir.read_from_string!() |> Stream.map(fn {k, "0x" <> hash} -> {k, Base.decode16!(hash, case: :mixed)} diff --git a/lib/lambda_ethereum_consensus/application.ex b/lib/lambda_ethereum_consensus/application.ex index 73276abcd..7635f1c2f 100644 --- a/lib/lambda_ethereum_consensus/application.ex +++ b/lib/lambda_ethereum_consensus/application.ex @@ -44,17 +44,12 @@ defmodule LambdaEthereumConsensus.Application do defp get_children(:full) do get_children(:db) ++ [ - {LambdaEthereumConsensus.Beacon.BeaconNode, [checkpoint_sync_url()]}, + LambdaEthereumConsensus.Beacon.BeaconNode, LambdaEthereumConsensus.P2P.Metadata, BeaconApi.Endpoint ] end - def checkpoint_sync_url do - Application.fetch_env!(:lambda_ethereum_consensus, LambdaEthereumConsensus.ForkChoice) - |> Keyword.fetch!(:checkpoint_sync_url) - end - defp get_operation_mode do Application.fetch_env!(:lambda_ethereum_consensus, LambdaEthereumConsensus) |> Keyword.fetch!(:mode) diff --git a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex index 2635155d7..4a0e4725e 100644 --- a/lib/lambda_ethereum_consensus/beacon/beacon_node.ex +++ b/lib/lambda_ethereum_consensus/beacon/beacon_node.ex @@ -4,50 +4,23 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do use Supervisor require Logger - alias LambdaEthereumConsensus.Beacon.CheckpointSync + alias LambdaEthereumConsensus.Beacon.StoreSetup alias LambdaEthereumConsensus.ForkChoice.Helpers alias LambdaEthereumConsensus.StateTransition.Cache - alias LambdaEthereumConsensus.StateTransition.Misc alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.BlockStates - alias LambdaEthereumConsensus.Store.StoreDb alias LambdaEthereumConsensus.Validator alias Types.BeaconState - alias Types.Store - - @max_epochs_before_stale 8 def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) end @impl true - def init([nil]) do - case restore_state_from_db() do - nil -> - Logger.error( - "[Sync] No recent state found. Please specify the URL to fetch them from via the --checkpoint-sync-url flag" - ) - - System.halt(1) - - {_, {store, root}} -> - init_children(store, root) - end - end - - def init([checkpoint_url]) do - case restore_state_from_db() do - {:ok, {store, root}} -> - Logger.warning("[Checkpoint sync] Recent state found. Ignoring the checkpoint URL.") - init_children(store, root) + def init(_) do + {store, genesis_validators_root} = StoreSetup.setup!() + deposit_tree_snapshot = StoreSetup.get_deposit_snapshot!() - _ -> - fetch_state_from_url(checkpoint_url) - end - end - - defp init_children(%Store{} = store, genesis_validators_root, deposit_tree_snapshot \\ nil) do Cache.initialize_cache() config = Application.fetch_env!(:lambda_ethereum_consensus, :discovery) @@ -93,72 +66,6 @@ defmodule LambdaEthereumConsensus.Beacon.BeaconNode do Supervisor.init(children, strategy: :one_for_all) end - defp restore_state_from_db do - # Try to fetch the old store from the database - case StoreDb.fetch_store() do - {:ok, %Store{finalized_checkpoint: %{epoch: finalized_epoch}} = store} -> - res = {store, ChainSpec.get_genesis_validators_root()} - - if get_current_epoch(store) - finalized_epoch > @max_epochs_before_stale do - Logger.info("[Sync] Found old state in DB.") - {:old_state, res} - else - Logger.info("[Sync] Found recent state in DB.") - {:ok, res} - end - - :not_found -> - nil - end - end - - defp fetch_state_from_url(url) do - Logger.info("[Checkpoint sync] Initiating checkpoint sync") - - genesis_validators_root = ChainSpec.get_genesis_validators_root() - - case CheckpointSync.get_finalized_block_and_state(url, genesis_validators_root) do - {:ok, {anchor_state, anchor_block}} -> - Logger.info( - "[Checkpoint sync] Received beacon state and block", - slot: anchor_state.slot - ) - - # We already checked block and state match - {:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block) - - # TODO: integrate into CheckpointSync, and validate snapshot - snapshot = fetch_deposit_snapshot(url) - - # Save store in DB - StoreDb.persist_store(store) - - init_children(store, genesis_validators_root, snapshot) - - _ -> - Logger.error("[Checkpoint sync] Failed to fetch the latest finalized state and block") - - System.halt(1) - end - end - - defp get_current_epoch(store) do - (:os.system_time(:second) - store.genesis_time) - |> div(ChainSpec.get("SECONDS_PER_SLOT")) - |> Misc.compute_epoch_at_slot() - end - - defp fetch_deposit_snapshot(url) do - case CheckpointSync.get_deposit_snapshot(url) do - {:ok, snapshot} -> - snapshot - - _ -> - Logger.error("[Checkpoint sync] Failed to fetch the deposit snapshot") - System.halt(1) - end - end - defp get_validator_children(nil, _, _, _) do Logger.warning("[Checkpoint sync] To enable validator features, checkpoint-sync is required.") diff --git a/lib/lambda_ethereum_consensus/beacon/store_setup.ex b/lib/lambda_ethereum_consensus/beacon/store_setup.ex new file mode 100644 index 000000000..b1efa3461 --- /dev/null +++ b/lib/lambda_ethereum_consensus/beacon/store_setup.ex @@ -0,0 +1,161 @@ +defmodule LambdaEthereumConsensus.Beacon.StoreSetup do + @moduledoc """ + Logic to get an initial state, store and deposit contract snapshot from different sources. + """ + + alias LambdaEthereumConsensus.Beacon.CheckpointSync + alias LambdaEthereumConsensus.StateTransition.Misc + alias LambdaEthereumConsensus.Store.StoreDb + alias Types.DepositTreeSnapshot + alias Types.Store + + @type store_setup_strategy :: + {:file, Types.BeaconState.t()} | {:checkpoint_sync_url, binary()} | :db + + require Logger + + @max_epochs_before_stale 8 + + @doc """ + Args: at least one can be nil. + - testnet_dir: directory of a testnet configuration, including ssz and yaml config. + - checkpoint_sync_url: a url where checkpoint sync can be performed. + + Return value: a store setup strategy, which is one of the following: + - {:file, anchor_state}: path of an ssz file to get the genesis state from. + - {:checkpoint_sync_url, url}: url to get the genesis state from if performing checkpoint sync. + - :db : the genesis state and store can only be recovered from the db. + """ + def make_strategy!(nil, nil), do: :db + def make_strategy!(nil, url) when is_binary(url), do: {:checkpoint_sync_url, url} + + def make_strategy!(dir, nil) when is_binary(dir) do + Path.join(dir, "genesis.ssz") + |> File.read!() + |> SszEx.decode(Types.BeaconState) + |> then(fn {:ok, state} -> {:file, state} end) + end + + @doc """ + Gets a {store, genesis_validators_root} tuple with the configured strategy. + """ + @spec setup!() :: {Store.t(), binary} + def setup!, do: setup!(get_strategy!()) + + @spec setup!(store_setup_strategy()) :: {Store.t(), binary} + def setup!({:file, anchor_state}) do + Logger.info("[Store Setup] Setting up store from genesis file.") + + default_block = SszEx.default(Types.SignedBeaconBlock) + state_root = Ssz.hash_tree_root!(anchor_state) + anchor_block = %{default_block | message: %{default_block.message | state_root: state_root}} + + {:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block) + {store, ChainSpec.get_genesis_validators_root()} + end + + def setup!({:checkpoint_sync_url, checkpoint_url}) do + case restore_state_from_db() do + {:ok, {store, root}} -> + Logger.warning("[Checkpoint sync] Recent state found. Ignoring the checkpoint URL.") + {store, root} + + _ -> + fetch_state_from_url(checkpoint_url) + end + end + + def setup!(:db) do + case restore_state_from_db() do + nil -> + Logger.error( + "[Sync] No recent state found. Please specify the URL to fetch them from via the --checkpoint-sync-url flag" + ) + + System.halt(1) + + {_, {store, root}} -> + {store, root} + end + end + + @doc """ + Gets the deposit tree snapshot. Will return nil unless the strategy is checkpoint sync. + """ + @spec get_deposit_snapshot!() :: DepositTreeSnapshot.t() | nil + def get_deposit_snapshot!, do: get_deposit_snapshot!(get_strategy!()) + + @spec get_deposit_snapshot!(store_setup_strategy()) :: DepositTreeSnapshot.t() | nil + def get_deposit_snapshot!({:file, _}), do: nil + def get_deposit_snapshot!({:checkpoint_sync_url, url}), do: fetch_deposit_snapshot(url) + def get_deposit_snapshot!(:db), do: nil + + @spec get_strategy!() :: store_setup_strategy + defp get_strategy! do + Application.get_env(:lambda_ethereum_consensus, __MODULE__) + |> Keyword.fetch!(:strategy) + end + + defp restore_state_from_db do + # Try to fetch the old store from the database + case StoreDb.fetch_store() do + {:ok, %Store{finalized_checkpoint: %{epoch: finalized_epoch}} = store} -> + res = {store, ChainSpec.get_genesis_validators_root()} + + if get_current_epoch(store) - finalized_epoch > @max_epochs_before_stale do + Logger.info("[Sync] Found old state in DB.") + {:old_state, res} + else + Logger.info("[Sync] Found recent state in DB.") + {:ok, res} + end + + :not_found -> + nil + end + end + + defp fetch_state_from_url(url) do + Logger.info("[Checkpoint sync] Initiating checkpoint sync") + + genesis_validators_root = ChainSpec.get_genesis_validators_root() + + case CheckpointSync.get_finalized_block_and_state(url, genesis_validators_root) do + {:ok, {anchor_state, anchor_block}} -> + Logger.info( + "[Checkpoint sync] Received beacon state and block", + slot: anchor_state.slot + ) + + # We already checked block and state match + {:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block) + + # Save store in DB + StoreDb.persist_store(store) + + {store, genesis_validators_root} + + _ -> + Logger.error("[Checkpoint sync] Failed to fetch the latest finalized state and block") + + System.halt(1) + end + end + + defp get_current_epoch(store) do + (:os.system_time(:second) - store.genesis_time) + |> div(ChainSpec.get("SECONDS_PER_SLOT")) + |> Misc.compute_epoch_at_slot() + end + + defp fetch_deposit_snapshot(url) do + case CheckpointSync.get_deposit_snapshot(url) do + {:ok, snapshot} -> + snapshot + + _ -> + Logger.error("[Checkpoint sync] Failed to fetch the deposit snapshot") + System.halt(1) + end + end +end