Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: checkpoint-sync-url now can be a list of checkpoint nodes #1411

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <your_url_here>
```

or you can specify mulitple urls by passing a comma separated list of urls:

```shell
iex -S mix run -- --checkpoint-sync-url "<url1>, <url2>, ..."
```

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]
Expand Down
8 changes: 7 additions & 1 deletion config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 58 additions & 16 deletions lib/lambda_ethereum_consensus/beacon/store_setup.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ 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
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")
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -129,28 +133,66 @@ 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")

# Fetch last finalized block for all urls
blocks = for {:ok, res} <- Enum.map(urls, &CheckpointSync.get_block/1), do: res

case Enum.uniq(blocks) do
[_] ->
Logger.info(
"[Checkpoin sync] Received the same last finalized block from #{length(blocks)} checkpoint nodes"
)

_ ->
Logger.error(
"[Checkpoint sync] Received inconsistent last finalized block from #{length(blocks)} checkpoint nodes"
)

Logger.flush()
System.halt(1)
end

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)

# 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)
Expand Down
Loading