From 8249fef3762a9f18caa858026bb4eef417de5f79 Mon Sep 17 00:00:00 2001 From: Leandro Serra Date: Mon, 31 Mar 2025 16:27:19 -0300 Subject: [PATCH 1/6] feat checkpoint-sync-url now can be a comma separated list of checkpoint nodes for comparisons --- config/runtime.exs | 8 ++- .../beacon/store_setup.ex | 60 ++++++++++++++----- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 58e242c5e..d6f0a6b4f 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -103,7 +103,13 @@ config :lambda_ethereum_consensus, LambdaEthereumConsensus.Store.Db, dir: datadi # 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) -strategy = StoreSetup.make_strategy!(testnet_dir, checkpoint_sync_url) +checkpoint_urls = + case checkpoint_sync_url do + urls when is_binary(urls) -> urls |> String.split(",") |> Enum.map(&String.trim/1) + nil -> nil + end + +strategy = StoreSetup.make_strategy!(testnet_dir, checkpoint_urls) genesis_validators_root = case strategy do diff --git a/lib/lambda_ethereum_consensus/beacon/store_setup.ex b/lib/lambda_ethereum_consensus/beacon/store_setup.ex index 154392966..9c3be36c2 100644 --- a/lib/lambda_ethereum_consensus/beacon/store_setup.ex +++ b/lib/lambda_ethereum_consensus/beacon/store_setup.ex @@ -28,7 +28,7 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do - :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!(nil, urls) when is_list(urls), do: {:checkpoint_sync_url, urls} def make_strategy!(dir, nil) when is_binary(dir) do Path.join(dir, "genesis.ssz") @@ -55,14 +55,14 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do store end - def setup!({:checkpoint_sync_url, checkpoint_url}) do + def setup!({:checkpoint_sync_url, checkpoint_urls}) do case restore_state_from_db() do {:ok, store} -> Logger.warning("[Checkpoint sync] Recent state found. Ignoring the checkpoint URL.") store _ -> - fetch_state_from_url(checkpoint_url) + fetch_and_compare_state_from_urls(checkpoint_urls) end end @@ -87,8 +87,12 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do @spec get_deposit_snapshot!() :: DepositTreeSnapshot.t() | nil def get_deposit_snapshot!(), do: get_deposit_snapshot!(get_strategy!()) + # The endpoint for deposit snapshots is deprecated in electra and will be removed in Fulu + # https://github.com/ethereum/beacon-APIs/pull/494 + # For this reason we don't compare the deposits from the urls as most checkpoints are returning error 500 @spec get_deposit_snapshot!(store_setup_strategy()) :: DepositTreeSnapshot.t() | nil - def get_deposit_snapshot!({:checkpoint_sync_url, url}), do: fetch_deposit_snapshot(url) + def get_deposit_snapshot!({:checkpoint_sync_url, urls}), + do: fetch_deposit_snapshot(List.first(urls)) def get_deposit_snapshot!(:db) do case StoreDb.fetch_deposits_snapshot() do @@ -129,28 +133,56 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do end end - defp fetch_state_from_url(url) do + defp fetch_and_compare_state_from_urls(urls) do Logger.info("[Checkpoint sync] Initiating checkpoint sync") genesis_validators_root = ChainSpec.get_genesis_validators_root() + states = + urls + |> Enum.map(&fetch_state_from_url(genesis_validators_root, &1)) + + case Enum.uniq(states) do + [_] -> + Logger.info( + "[Checkpoin sync] Received the same state from #{length(states)} checkpoint nodes" + ) + + _ -> + Logger.error( + "[Checkpoint sync] Received inconsistent states from #{length(states)} checkpoint nodes" + ) + + Logger.flush() + System.halt(1) + end + + # All states are the same so we can use the first one + {anchor_state, anchor_block} = List.first(states) + + # 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 + end + + defp fetch_state_from_url(genesis_validators_root, url) do 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", + "[Checkpoint sync] Received beacon state and block from URL #{url}", 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 + {anchor_state, anchor_block} _ -> - Logger.error("[Checkpoint sync] Failed to fetch the latest finalized state and block") + Logger.error( + "[Checkpoint sync] Failed to fetch the latest finalized state and block for URL: #{url}" + ) Logger.flush() System.halt(1) From 0b6a8677122fb5773705c70aa93c7675ed6704ca Mon Sep 17 00:00:00 2001 From: Leandro Serra Date: Wed, 26 Mar 2025 15:03:43 -0300 Subject: [PATCH 2/6] try ci fix --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1efd25f54..603376fd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # libp2p port -FROM golang:1.21.3 AS libp2p_builder +FROM golang:1.22 AS libp2p_builder LABEL stage=builder # Install dependencies From 172e790812fdfc148018d69272901fd66a034a65 Mon Sep 17 00:00:00 2001 From: Leandro Serra Date: Wed, 26 Mar 2025 15:15:32 -0300 Subject: [PATCH 3/6] bump go version to 1.22 --- .tool-versions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index ec2ffeec7..73dd5a9c6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ erlang 26.2 elixir 1.16.2-otp-26 -golang 1.21.3 +golang 1.22 rust 1.81.0 protoc 24.3 From b557e2d54a1de66505b6ba6b2dccc34e0bb18f8f Mon Sep 17 00:00:00 2001 From: Leandro Serra Date: Tue, 1 Apr 2025 09:18:18 -0300 Subject: [PATCH 4/6] update documentation, and code comments --- README.md | 8 ++++++++ lib/lambda_ethereum_consensus/beacon/store_setup.ex | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 128db54a5..bbcc81097 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,14 @@ You can specify a URL to fetch it from with the "--checkpoint-sync-url" flag: iex -S mix run -- --checkpoint-sync-url ``` +or you can specify mulitple urls by passing a comma separated list of urls: + +```shell +iex -S mix run -- --checkpoint-sync-url ", , ..." +``` + +If multiple urls are provided the downloaded state will be compared for all urls and fail if even one of them differs from the rest + Some public endpoints can be found in [eth-clients.github.io/checkpoint-sync-endpoints](https://eth-clients.github.io/checkpoint-sync-endpoints/). > [!IMPORTANT] diff --git a/lib/lambda_ethereum_consensus/beacon/store_setup.ex b/lib/lambda_ethereum_consensus/beacon/store_setup.ex index 9c3be36c2..21ba4ca50 100644 --- a/lib/lambda_ethereum_consensus/beacon/store_setup.ex +++ b/lib/lambda_ethereum_consensus/beacon/store_setup.ex @@ -20,11 +20,11 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do @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. + - checkpoint_sync_url: list of urls 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. + - {:checkpoint_sync_url, url}: list of urls 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 From 0c79a6dbc8a7b6addaeb09bbb463f99ef1188e2d Mon Sep 17 00:00:00 2001 From: Leandro Serra Date: Tue, 1 Apr 2025 17:49:35 -0300 Subject: [PATCH 5/6] fix only download latest finalized block from multiple urls download state only from one url --- .../beacon/store_setup.ex | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/store_setup.ex b/lib/lambda_ethereum_consensus/beacon/store_setup.ex index 21ba4ca50..ba70623a1 100644 --- a/lib/lambda_ethereum_consensus/beacon/store_setup.ex +++ b/lib/lambda_ethereum_consensus/beacon/store_setup.ex @@ -136,29 +136,39 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do defp fetch_and_compare_state_from_urls(urls) do Logger.info("[Checkpoint sync] Initiating checkpoint sync") - genesis_validators_root = ChainSpec.get_genesis_validators_root() - - states = - urls - |> Enum.map(&fetch_state_from_url(genesis_validators_root, &1)) + # Fetch last finalized block for all urls + blocks = for {:ok, res} <- Enum.map(urls, &CheckpointSync.get_block/1), do: res - case Enum.uniq(states) do + case Enum.uniq(blocks) do [_] -> Logger.info( - "[Checkpoin sync] Received the same state from #{length(states)} checkpoint nodes" + "[Checkpoin sync] Received the same last finalized block from #{length(blocks)} checkpoint nodes" ) _ -> Logger.error( - "[Checkpoint sync] Received inconsistent states from #{length(states)} checkpoint nodes" + "[Checkpoint sync] Received inconsistent last finalized block from #{length(blocks)} checkpoint nodes" ) Logger.flush() System.halt(1) end - # All states are the same so we can use the first one - {anchor_state, anchor_block} = List.first(states) + genesis_validators_root = ChainSpec.get_genesis_validators_root() + + # All urls returned the same last finalized block, we will trust the first to get the state + {anchor_state, anchor_block} = fetch_state_from_url(genesis_validators_root, List.first(urls)) + + first_block = List.first(blocks) + + if anchor_state.latest_block_header.parent_root != first_block.message.parent_root do + Logger.error( + "[Checkpoint sync] Root mismatch when comparing latest finalized block with downloaded state" + ) + + Logger.flush() + System.halt(1) + end # We already checked block and state match {:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block) From e092fd140d226ed049fa7702039ee5c23a938ad4 Mon Sep 17 00:00:00 2001 From: Leandro Serra Date: Mon, 21 Apr 2025 09:19:40 -0300 Subject: [PATCH 6/6] refactor fetch_and_compare_state_from_urls error logic --- .../beacon/store_setup.ex | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/lambda_ethereum_consensus/beacon/store_setup.ex b/lib/lambda_ethereum_consensus/beacon/store_setup.ex index ba70623a1..833a9724a 100644 --- a/lib/lambda_ethereum_consensus/beacon/store_setup.ex +++ b/lib/lambda_ethereum_consensus/beacon/store_setup.ex @@ -161,7 +161,14 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do first_block = List.first(blocks) - if anchor_state.latest_block_header.parent_root != first_block.message.parent_root do + if anchor_state.latest_block_header.parent_root == first_block.message.parent_root do + {:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block) + + # Save store in DB + StoreDb.persist_store(store) + + store + else Logger.error( "[Checkpoint sync] Root mismatch when comparing latest finalized block with downloaded state" ) @@ -169,14 +176,6 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do Logger.flush() System.halt(1) end - - # 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 end defp fetch_state_from_url(genesis_validators_root, url) do