Skip to content

Commit 53c4585

Browse files
feat: assertoor implementation support (#1324)
Co-authored-by: ElFantasma <estebandh@gmail.com>
1 parent 87a05cf commit 53c4585

File tree

13 files changed

+284
-34
lines changed

13 files changed

+284
-34
lines changed

.github/config/assertoor/network-params.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ participants:
2020
keymanager_enabled: true
2121

2222
additional_services:
23-
# - assertoor
23+
- assertoor
2424
- tx_spammer
25+
- blob_spammer
2526
- dora
2627

27-
#assertoor_params:
28-
# run_stability_check: false
29-
# run_block_proposal_check: false
30-
# tests: []
31-
# - https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_consensus/refs/heads/main/.github/config/assertoor/cl-stability-check.yml
28+
assertoor_params:
29+
run_stability_check: false
30+
run_block_proposal_check: false
31+
tests:
32+
- https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_consensus/refs/heads/main/.github/config/assertoor/cl-stability-check.yml
3233

3334
tx_spammer_params:
3435
tx_spammer_extra_args: ["--txcount=3", "--accounts=80"]

lib/beacon_api/controllers/error_controller.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
defmodule BeaconApi.ErrorController do
2+
require Logger
23
use BeaconApi, :controller
34

45
@spec bad_request(Plug.Conn.t(), binary()) :: Plug.Conn.t()
56
def bad_request(conn, message) do
7+
Logger.error("Bad request: #{message}, path: #{conn.request_path}")
8+
69
conn
710
|> put_status(400)
811
|> json(%{
@@ -13,6 +16,8 @@ defmodule BeaconApi.ErrorController do
1316

1417
@spec not_found(Plug.Conn.t(), any) :: Plug.Conn.t()
1518
def not_found(conn, _params) do
19+
Logger.error("Resource not found, path: #{conn.request_path}")
20+
1621
conn
1722
|> put_status(404)
1823
|> json(%{
@@ -23,6 +28,8 @@ defmodule BeaconApi.ErrorController do
2328

2429
@spec internal_error(Plug.Conn.t(), any) :: Plug.Conn.t()
2530
def internal_error(conn, _params) do
31+
Logger.error("Internal server error, path: #{conn.request_path}")
32+
2633
conn
2734
|> put_status(500)
2835
|> json(%{

lib/beacon_api/controllers/v1/beacon_controller.ex

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ defmodule BeaconApi.V1.BeaconController do
2525
def open_api_operation(:get_finality_checkpoints),
2626
do: ApiSpec.spec().paths["/eth/v1/beacon/states/{state_id}/finality_checkpoints"].get
2727

28+
def open_api_operation(:get_headers_by_block),
29+
do: ApiSpec.spec().paths["/eth/v1/beacon/headers/{block_id}"].get
30+
2831
@spec get_genesis(Plug.Conn.t(), any) :: Plug.Conn.t()
2932
def get_genesis(conn, _params) do
3033
conn
3134
|> json(%{
3235
"data" => %{
33-
"genesis_time" => StoreDb.fetch_genesis_time!(),
36+
"genesis_time" => StoreDb.fetch_genesis_time!() |> Integer.to_string(),
3437
"genesis_validators_root" =>
3538
ChainSpec.get_genesis_validators_root() |> Utils.hex_encode(),
3639
"genesis_fork_version" => ChainSpec.get("GENESIS_FORK_VERSION") |> Utils.hex_encode()
@@ -168,18 +171,52 @@ defmodule BeaconApi.V1.BeaconController do
168171
finalized: finalized,
169172
data: %{
170173
previous_justified: %{
171-
epoch: previous_justified_checkpoint.epoch,
174+
epoch: previous_justified_checkpoint.epoch |> Integer.to_string(),
172175
root: Utils.hex_encode(previous_justified_checkpoint.root)
173176
},
174177
current_justified: %{
175-
epoch: current_justified_checkpoint.epoch,
178+
epoch: current_justified_checkpoint.epoch |> Integer.to_string(),
176179
root: Utils.hex_encode(current_justified_checkpoint.root)
177180
},
178181
finalized: %{
179-
epoch: finalized_checkpoint.epoch,
182+
epoch: finalized_checkpoint.epoch |> Integer.to_string(),
180183
root: Utils.hex_encode(finalized_checkpoint.root)
181184
}
182185
}
183186
})
184187
end
188+
189+
@spec get_headers_by_block(Plug.Conn.t(), any) :: Plug.Conn.t()
190+
def get_headers_by_block(conn, %{block_id: "head"}) do
191+
{:ok, store} = StoreDb.fetch_store()
192+
head_root = store.head_root
193+
%{signed_block: %{message: message, signature: signature}} = Blocks.get_block_info(head_root)
194+
195+
conn
196+
# TODO: This is a placeholder, a minimum implementation to make assertoor run
197+
|> json(%{
198+
execution_optimistic: false,
199+
200+
# This is obviously false for the head, but should be derived
201+
finalized: false,
202+
data: %{
203+
root: head_root |> Utils.hex_encode(),
204+
205+
# This needs to be derived
206+
canonical: true,
207+
header: %{
208+
message: %{
209+
slot: message.slot |> Integer.to_string(),
210+
proposer_index: message.proposer_index |> Integer.to_string(),
211+
parent_root: message.parent_root |> Utils.hex_encode(),
212+
state_root: message.state_root |> Utils.hex_encode(),
213+
body_root: SszEx.hash_tree_root!(message.body) |> Utils.hex_encode()
214+
},
215+
signature: signature |> Utils.hex_encode()
216+
}
217+
}
218+
})
219+
end
220+
221+
def get_headers_by_block(conn, _params), do: conn |> ErrorController.not_found(nil)
185222
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
defmodule BeaconApi.V1.ConfigController do
2+
use BeaconApi, :controller
3+
require Logger
4+
5+
alias BeaconApi.ApiSpec
6+
alias BeaconApi.Utils
7+
8+
plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true)
9+
10+
@chain_spec_removed_keys [
11+
"ATTESTATION_SUBNET_COUNT",
12+
"KZG_COMMITMENT_INCLUSION_PROOF_DEPTH",
13+
"UPDATE_TIMEOUT"
14+
]
15+
@chain_spec_renamed_keys [
16+
{"MAXIMUM_GOSSIP_CLOCK_DISPARITY", "MAXIMUM_GOSSIP_CLOCK_DISPARITY_MILLIS"}
17+
]
18+
@chain_spec_hex_fields [
19+
"TERMINAL_BLOCK_HASH",
20+
"GENESIS_FORK_VERSION",
21+
"ALTAIR_FORK_VERSION",
22+
"BELLATRIX_FORK_VERSION",
23+
"CAPELLA_FORK_VERSION",
24+
"DENEB_FORK_VERSION",
25+
"ELECTRA_FORK_VERSION",
26+
"DEPOSIT_CONTRACT_ADDRESS",
27+
"MESSAGE_DOMAIN_INVALID_SNAPPY",
28+
"MESSAGE_DOMAIN_VALID_SNAPPY"
29+
]
30+
31+
# NOTE: this function is required by OpenApiSpex, and should return the information
32+
# of each specific endpoint. We just return the specific entry from the parsed spec.
33+
def open_api_operation(:get_spec),
34+
do: ApiSpec.spec().paths["/eth/v1/config/spec"].get
35+
36+
# TODO: This is still an incomplete implementation, it should return some constants
37+
# along with the chain spec. It's enough for assertoor.
38+
@spec get_spec(Plug.Conn.t(), any) :: Plug.Conn.t()
39+
def get_spec(conn, _params), do: json(conn, %{"data" => chain_spec()})
40+
41+
defp chain_spec() do
42+
ChainSpec.get_all()
43+
|> Map.drop(@chain_spec_removed_keys)
44+
|> rename_keys(@chain_spec_renamed_keys)
45+
|> Map.new(fn
46+
{k, v} when is_integer(v) -> {k, Integer.to_string(v)}
47+
{k, v} when k in @chain_spec_hex_fields -> {k, Utils.hex_encode(v)}
48+
{k, v} -> {k, v}
49+
end)
50+
end
51+
52+
defp rename_keys(config, renamed_keys) do
53+
renamed_keys
54+
|> Enum.reduce(config, fn {old_key, new_key}, config ->
55+
case Map.get(config, old_key) do
56+
nil -> config
57+
value -> Map.put_new(config, new_key, value) |> Map.delete(old_key)
58+
end
59+
end)
60+
end
61+
end

lib/beacon_api/controllers/v1/node_controller.ex

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,21 @@ defmodule BeaconApi.V1.NodeController do
1919
def open_api_operation(:version),
2020
do: ApiSpec.spec().paths["/eth/v1/node/version"].get
2121

22+
def open_api_operation(:syncing),
23+
do: ApiSpec.spec().paths["/eth/v1/node/syncing"].get
24+
25+
def open_api_operation(:peers),
26+
do: ApiSpec.spec().paths["/eth/v1/node/peers"].get
27+
2228
@spec health(Plug.Conn.t(), any) :: Plug.Conn.t()
2329
def health(conn, params) do
24-
# TODO: respond with syncing status if we're still syncing
25-
_syncing_status = Map.get(params, :syncing_status, 206)
30+
%{syncing?: syncing?} = Libp2pPort.sync_status()
31+
32+
syncing_status = if syncing?, do: Map.get(params, :syncing_status, 206), else: 200
2633

27-
send_resp(conn, 200, "")
34+
send_resp(conn, syncing_status, "")
35+
rescue
36+
_ -> send_resp(conn, 503, "")
2837
end
2938

3039
@spec identity(Plug.Conn.t(), any) :: Plug.Conn.t()
@@ -62,4 +71,25 @@ defmodule BeaconApi.V1.NodeController do
6271
}
6372
})
6473
end
74+
75+
@spec syncing(Plug.Conn.t(), any) :: Plug.Conn.t()
76+
def syncing(conn, _params) do
77+
%{
78+
syncing?: is_syncing,
79+
optimistic?: is_optimistic,
80+
el_offline?: el_offline,
81+
head_slot: head_slot,
82+
sync_distance: sync_distance
83+
} = Libp2pPort.sync_status()
84+
85+
json(conn, %{
86+
"data" => %{
87+
"is_syncing" => is_syncing,
88+
"is_optimistic" => is_optimistic,
89+
"el_offline" => el_offline,
90+
"head_slot" => head_slot |> Integer.to_string(),
91+
"sync_distance" => sync_distance |> Integer.to_string()
92+
}
93+
})
94+
end
6595
end

lib/beacon_api/helpers.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,14 @@ defmodule BeaconApi.Helpers do
154154
@spec finality_checkpoint_by_id(state_id()) ::
155155
{:ok, finality_info()} | {:error, String.t()} | :not_found | :empty_slot | :invalid_id
156156
def finality_checkpoint_by_id(id) do
157+
empty_checkpoint = %Types.Checkpoint{epoch: 0, root: <<0::256>>}
158+
157159
with {:ok, {state, optimistic, finalized}} <- state_by_state_id(id) do
158-
{:ok,
159-
{state.previous_justified_checkpoint, state.current_justified_checkpoint,
160-
state.finalized_checkpoint, optimistic, finalized}}
160+
previous_justified_ck = Map.get(state, :previous_justified_checkpoint, empty_checkpoint)
161+
current_justified_ck = Map.get(state, :current_justified_checkpoint, empty_checkpoint)
162+
finalized_ck = Map.get(state, :finalized_checkpoint, empty_checkpoint)
163+
164+
{:ok, {previous_justified_ck, current_justified_ck, finalized_ck, optimistic, finalized}}
161165
end
162166
end
163167

lib/beacon_api/router.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
defmodule BeaconApi.Router do
22
use BeaconApi, :router
3+
require Logger
34

45
pipeline :api do
56
plug(:accepts, ["json", "sse"])
67
plug(OpenApiSpex.Plug.PutApiSpec, module: BeaconApi.ApiSpec)
8+
plug(:log_requests)
79
end
810

911
# Ethereum API Version 1
@@ -15,12 +17,19 @@ defmodule BeaconApi.Router do
1517
get("/states/:state_id/root", BeaconController, :get_state_root)
1618
get("/blocks/:block_id/root", BeaconController, :get_block_root)
1719
get("/states/:state_id/finality_checkpoints", BeaconController, :get_finality_checkpoints)
20+
get("/headers/:block_id", BeaconController, :get_headers_by_block)
21+
end
22+
23+
scope "/config" do
24+
get("/spec", ConfigController, :get_spec)
1825
end
1926

2027
scope "/node" do
2128
get("/health", NodeController, :health)
2229
get("/identity", NodeController, :identity)
2330
get("/version", NodeController, :version)
31+
get("/syncing", NodeController, :syncing)
32+
get("/peers", NodeController, :peers)
2433
end
2534

2635
scope "/events" do
@@ -44,4 +53,16 @@ defmodule BeaconApi.Router do
4453

4554
# Catch-all route outside of any scope
4655
match(:*, "/*path", BeaconApi.ErrorController, :not_found)
56+
57+
defp log_requests(conn, _opts) do
58+
base_message = "[BeaconAPI Router] Processing request: #{conn.method} - #{conn.request_path}"
59+
query = if conn.query_params != %{}, do: "Query: #{inspect(conn.query_params)}", else: ""
60+
body = if conn.body_params != %{}, do: "Body: #{inspect(conn.body_params)}", else: ""
61+
62+
[base_message, query, body]
63+
|> Enum.join("\n\t")
64+
|> Logger.info()
65+
66+
conn
67+
end
4768
end

lib/chain_spec/chain_spec.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ defmodule ChainSpec do
1919
# NOTE: this only works correctly for Capella
2020
def get(name), do: get_config().get(name)
2121

22+
def get_all(), do: get_config().get_all()
23+
2224
def get_genesis_validators_root() do
2325
Application.fetch_env!(:lambda_ethereum_consensus, __MODULE__)
2426
|> Keyword.fetch!(:genesis_validators_root)

lib/lambda_ethereum_consensus/beacon/sync_blocks.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,10 @@ defmodule LambdaEthereumConsensus.Beacon.SyncBlocks do
2020
finish, each block of those responses will be sent to libp2p port module individually using
2121
Libp2pPort.add_block/1.
2222
"""
23-
@spec run() :: non_neg_integer()
24-
def run() do
25-
%{head_slot: head_slot} = ForkChoice.get_current_status_message()
23+
@spec run(Types.Store.t()) :: non_neg_integer()
24+
def run(%{head_slot: head_slot} = store) do
2625
initial_slot = head_slot + 1
27-
last_slot = ForkChoice.get_current_chain_slot()
26+
last_slot = ForkChoice.get_current_slot(store)
2827

2928
# If we're around genesis, we consider ourselves synced
3029
if last_slot <= 0 do

lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,7 @@ defmodule LambdaEthereumConsensus.ForkChoice do
125125
@doc """
126126
Get the current chain slot based on the system time.
127127
128-
There are just 2 uses of this function outside this module:
129-
- At the begining of SyncBlocks.run/1 function, to get the head slot
128+
There is just 1 use of this function outside this module:
130129
- In the Helpers.block_root_by_block_id/1 function
131130
"""
132131
@spec get_current_chain_slot() :: Types.slot()

0 commit comments

Comments
 (0)