Skip to content

Commit a9735c8

Browse files
authored
feat: checkpoint-sync-url now can be a list of checkpoint nodes (#1411)
1 parent 65fa702 commit a9735c8

File tree

3 files changed

+72
-17
lines changed

3 files changed

+72
-17
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ You can specify a URL to fetch it from with the "--checkpoint-sync-url" flag:
106106
iex -S mix run -- --checkpoint-sync-url <your_url_here>
107107
```
108108

109+
or you can specify mulitple urls by passing a comma separated list of urls:
110+
111+
```shell
112+
iex -S mix run -- --checkpoint-sync-url "<url1>, <url2>, ..."
113+
```
114+
115+
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
116+
109117
Some public endpoints can be found in [eth-clients.github.io/checkpoint-sync-endpoints](https://eth-clients.github.io/checkpoint-sync-endpoints/).
110118

111119
> [!IMPORTANT]

config/runtime.exs

+7-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ config :lambda_ethereum_consensus, LambdaEthereumConsensus.Store.Db, dir: datadi
103103
# We use put_env here as we need this immediately after to read the state.
104104
Application.put_env(:lambda_ethereum_consensus, ChainSpec, config: chain_config)
105105

106-
strategy = StoreSetup.make_strategy!(testnet_dir, checkpoint_sync_url)
106+
checkpoint_urls =
107+
case checkpoint_sync_url do
108+
urls when is_binary(urls) -> urls |> String.split(",") |> Enum.map(&String.trim/1)
109+
nil -> nil
110+
end
111+
112+
strategy = StoreSetup.make_strategy!(testnet_dir, checkpoint_urls)
107113

108114
genesis_validators_root =
109115
case strategy do

lib/lambda_ethereum_consensus/beacon/store_setup.ex

+57-16
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do
2020
@doc """
2121
Args: at least one can be nil.
2222
- testnet_dir: directory of a testnet configuration, including ssz and yaml config.
23-
- checkpoint_sync_url: a url where checkpoint sync can be performed.
23+
- checkpoint_sync_url: list of urls where checkpoint sync can be performed.
2424
2525
Return value: a store setup strategy, which is one of the following:
2626
- {:file, anchor_state}: path of an ssz file to get the genesis state from.
27-
- {:checkpoint_sync_url, url}: url to get the genesis state from if performing checkpoint sync.
27+
- {:checkpoint_sync_url, url}: list of urls to get the genesis state from if performing checkpoint sync.
2828
- :db : the genesis state and store can only be recovered from the db.
2929
"""
3030
def make_strategy!(nil, nil), do: :db
31-
def make_strategy!(nil, url) when is_binary(url), do: {:checkpoint_sync_url, url}
31+
def make_strategy!(nil, urls) when is_list(urls), do: {:checkpoint_sync_url, urls}
3232

3333
def make_strategy!(dir, nil) when is_binary(dir) do
3434
Path.join(dir, "genesis.ssz")
@@ -55,14 +55,14 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do
5555
store
5656
end
5757

58-
def setup!({:checkpoint_sync_url, checkpoint_url}) do
58+
def setup!({:checkpoint_sync_url, checkpoint_urls}) do
5959
case restore_state_from_db() do
6060
{:ok, store} ->
6161
Logger.warning("[Checkpoint sync] Recent state found. Ignoring the checkpoint URL.")
6262
store
6363

6464
_ ->
65-
fetch_state_from_url(checkpoint_url)
65+
fetch_and_compare_state_from_urls(checkpoint_urls)
6666
end
6767
end
6868

@@ -87,8 +87,12 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do
8787
@spec get_deposit_snapshot!() :: DepositTreeSnapshot.t() | nil
8888
def get_deposit_snapshot!(), do: get_deposit_snapshot!(get_strategy!())
8989

90+
# The endpoint for deposit snapshots is deprecated in electra and will be removed in Fulu
91+
# https://github.com/ethereum/beacon-APIs/pull/494
92+
# For this reason we don't compare the deposits from the urls as most checkpoints are returning error 500
9093
@spec get_deposit_snapshot!(store_setup_strategy()) :: DepositTreeSnapshot.t() | nil
91-
def get_deposit_snapshot!({:checkpoint_sync_url, url}), do: fetch_deposit_snapshot(url)
94+
def get_deposit_snapshot!({:checkpoint_sync_url, urls}),
95+
do: fetch_deposit_snapshot(List.first(urls))
9296

9397
def get_deposit_snapshot!(:db) do
9498
case StoreDb.fetch_deposits_snapshot() do
@@ -129,28 +133,65 @@ defmodule LambdaEthereumConsensus.Beacon.StoreSetup do
129133
end
130134
end
131135

132-
defp fetch_state_from_url(url) do
136+
defp fetch_and_compare_state_from_urls(urls) do
133137
Logger.info("[Checkpoint sync] Initiating checkpoint sync")
134138

139+
# Fetch last finalized block for all urls
140+
blocks = for {:ok, res} <- Enum.map(urls, &CheckpointSync.get_block/1), do: res
141+
142+
case Enum.uniq(blocks) do
143+
[_] ->
144+
Logger.info(
145+
"[Checkpoin sync] Received the same last finalized block from #{length(blocks)} checkpoint nodes"
146+
)
147+
148+
_ ->
149+
Logger.error(
150+
"[Checkpoint sync] Received inconsistent last finalized block from #{length(blocks)} checkpoint nodes"
151+
)
152+
153+
Logger.flush()
154+
System.halt(1)
155+
end
156+
135157
genesis_validators_root = ChainSpec.get_genesis_validators_root()
136158

159+
# All urls returned the same last finalized block, we will trust the first to get the state
160+
{anchor_state, anchor_block} = fetch_state_from_url(genesis_validators_root, List.first(urls))
161+
162+
first_block = List.first(blocks)
163+
164+
if anchor_state.latest_block_header.parent_root == first_block.message.parent_root do
165+
{:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block)
166+
167+
# Save store in DB
168+
StoreDb.persist_store(store)
169+
170+
store
171+
else
172+
Logger.error(
173+
"[Checkpoint sync] Root mismatch when comparing latest finalized block with downloaded state"
174+
)
175+
176+
Logger.flush()
177+
System.halt(1)
178+
end
179+
end
180+
181+
defp fetch_state_from_url(genesis_validators_root, url) do
137182
case CheckpointSync.get_finalized_block_and_state(url, genesis_validators_root) do
138183
{:ok, {anchor_state, anchor_block}} ->
139184
Logger.info(
140-
"[Checkpoint sync] Received beacon state and block",
185+
"[Checkpoint sync] Received beacon state and block from URL #{url}",
141186
slot: anchor_state.slot
142187
)
143188

144-
# We already checked block and state match
145-
{:ok, store} = Store.get_forkchoice_store(anchor_state, anchor_block)
146-
147-
# Save store in DB
148-
StoreDb.persist_store(store)
149-
150-
store
189+
{anchor_state, anchor_block}
151190

152191
_ ->
153-
Logger.error("[Checkpoint sync] Failed to fetch the latest finalized state and block")
192+
Logger.error(
193+
"[Checkpoint sync] Failed to fetch the latest finalized state and block for URL: #{url}"
194+
)
154195

155196
Logger.flush()
156197
System.halt(1)

0 commit comments

Comments
 (0)