From a7814b4b0ba5aa566a8ade05148af82458e0b002 Mon Sep 17 00:00:00 2001 From: Fernando Ledesma Date: Mon, 22 Jan 2024 12:14:57 +0000 Subject: [PATCH] feat: add open api spex beacon endpoints (#635) --- .github/workflows/ci.yml | 48 +++++++++++++++++-- .gitignore | 3 ++ .oapi_version | 1 + Makefile | 15 +++++- lib/beacon_api/api_spec.ex | 15 ++++++ .../controllers/v1/beacon_controller.ex | 31 +++++++++--- .../controllers/v2/beacon_controller.ex | 25 +++++++--- lib/beacon_api/router.ex | 6 +++ mix.exs | 3 +- mix.lock | 1 + 10 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 .oapi_version create mode 100644 lib/beacon_api/api_spec.ex diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce3f1bb50..eeea4b899 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,9 +51,25 @@ jobs: if: steps.output-cache.outputs.cache-hit != 'true' run: make compile-port compile-native + download-beacon-node-oapi: + name: Download Beacon Node OAPI + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Cache Beacon Node OAPI + id: output-cache + uses: actions/cache@v3 + with: + path: ./beacon-node-oapi.json + key: ${{ runner.os }}-beacon-node-oapi-${{ hashFiles('.oapi_version') }} + lookup-only: true + - name: Download Beacon Node OAPI + if: steps.output-cache.outputs.cache-hit != 'true' + run: make download-beacon-node-oapi + build: name: Build project - needs: compile-native + needs: [compile-native, download-beacon-node-oapi] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -75,6 +91,12 @@ jobs: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} restore-keys: ${{ runner.os }}-mix- + - name: Fetch beacon node oapi file + uses: actions/cache/restore@v3 + with: + path: ./beacon-node-oapi.json + key: ${{ runner.os }}-beacon-node-oapi-${{ hashFiles('.oapi_version') }} + fail-on-cache-miss: true - name: Install dependencies run: | sudo apt-get install -y protobuf-compiler @@ -104,7 +126,7 @@ jobs: smoke: name: Start and stop the node - needs: compile-native + needs: [compile-native, download-beacon-node-oapi] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -126,6 +148,12 @@ jobs: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} restore-keys: ${{ runner.os }}-mix- + - name: Fetch beacon node oapi file + uses: actions/cache/restore@v3 + with: + path: ./beacon-node-oapi.json + key: ${{ runner.os }}-beacon-node-oapi-${{ hashFiles('.oapi_version') }} + fail-on-cache-miss: true - name: Install dependencies run: | sudo apt-get install -y protobuf-compiler @@ -145,7 +173,7 @@ jobs: test: name: Test - needs: compile-native + needs: [compile-native, download-beacon-node-oapi] runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -167,6 +195,12 @@ jobs: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} restore-keys: ${{ runner.os }}-mix- + - name: Fetch beacon node oapi file + uses: actions/cache/restore@v3 + with: + path: ./beacon-node-oapi.json + key: ${{ runner.os }}-beacon-node-oapi-${{ hashFiles('.oapi_version') }} + fail-on-cache-miss: true - name: Set up cargo cache uses: Swatinem/rust-cache@v2 with: @@ -223,7 +257,7 @@ jobs: spectests: name: Run spec-tests - needs: [compile-native, download-spectests] + needs: [compile-native, download-spectests, download-beacon-node-oapi] strategy: matrix: config: ["minimal", "general", "mainnet"] @@ -253,6 +287,12 @@ jobs: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} restore-keys: ${{ runner.os }}-mix- + - name: Fetch beacon node oapi file + uses: actions/cache/restore@v3 + with: + path: ./beacon-node-oapi.json + key: ${{ runner.os }}-beacon-node-oapi-${{ hashFiles('.oapi_version') }} + fail-on-cache-miss: true - name: Set up cargo cache uses: Swatinem/rust-cache@v2 with: diff --git a/.gitignore b/.gitignore index bb7281b4d..f417f1396 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ priv # profiling artifacts callgrind.out.* *-eflambe-output.bggg + +# beacon node oapi json file +beacon-node-oapi.json diff --git a/.oapi_version b/.oapi_version new file mode 100644 index 000000000..3dfbe3369 --- /dev/null +++ b/.oapi_version @@ -0,0 +1 @@ +v2.4.2 diff --git a/Makefile b/Makefile index 819360f3a..0c822c710 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ clean-vectors download-vectors uncompress-vectors proto \ spec-test-% spec-test spec-test-config-% spec-test-runner-% \ spec-test-mainnet-% spec-test-minimal-% spec-test-general-% \ - clean-tests gen-spec compile-all + clean-tests gen-spec compile-all download-beacon-node-oapi # Delete current file when command fails .DELETE_ON_ERROR: @@ -86,7 +86,7 @@ proto: $(PROTOBUF_EX_FILES) $(PROTOBUF_GO_FILES) compile-native: $(OUTPUT_DIR)/libp2p_nif.so $(OUTPUT_DIR)/libp2p_port #🔨 compile-all: @ Compile the elixir project and its dependencies. -compile-all: compile-native $(PROTOBUF_EX_FILES) +compile-all: compile-native $(PROTOBUF_EX_FILES) download-beacon-node-oapi mix compile #🗑️ clean: @ Remove the build files. @@ -126,6 +126,17 @@ sepolia: compile-all test: compile-all mix test --no-start --exclude spectest +#### BEACON NODE OAPI #### +OAPI_NAME = beacon-node-oapi +OAPI_VERSION := $(shell cat .oapi_version) +$(OAPI_NAME).json: .oapi_version + curl -L -o "$@" \ + "https://ethereum.github.io/beacon-APIs/releases/${OAPI_VERSION}/beacon-node-oapi.json" + +OPENAPI_JSON := $(OAPI_NAME).json + +download-beacon-node-oapi: ${OPENAPI_JSON} + ##### SPEC TEST VECTORS ##### SPECTEST_VERSION := $(shell cat .spectest_version) diff --git a/lib/beacon_api/api_spec.ex b/lib/beacon_api/api_spec.ex new file mode 100644 index 000000000..5d0853b40 --- /dev/null +++ b/lib/beacon_api/api_spec.ex @@ -0,0 +1,15 @@ +defmodule BeaconApi.ApiSpec do + @moduledoc false + alias OpenApiSpex.OpenApi + @behaviour OpenApi + + file = "beacon-node-oapi.json" + @external_resource file + @ethspec file + |> File.read!() + |> Jason.decode!() + |> OpenApiSpex.OpenApi.Decode.decode() + + @impl OpenApi + def spec, do: @ethspec +end diff --git a/lib/beacon_api/controllers/v1/beacon_controller.ex b/lib/beacon_api/controllers/v1/beacon_controller.ex index d95f2cc70..ff5fb64a1 100644 --- a/lib/beacon_api/controllers/v1/beacon_controller.ex +++ b/lib/beacon_api/controllers/v1/beacon_controller.ex @@ -1,11 +1,25 @@ defmodule BeaconApi.V1.BeaconController do + alias BeaconApi.ApiSpec alias BeaconApi.ErrorController + alias LambdaEthereumConsensus.ForkChoice alias LambdaEthereumConsensus.Store.BlockStore use BeaconApi, :controller + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + @doc """ + action is an atom that correspond to the controller action's function atoms declared on `BeaconApi.Router` + """ + def open_api_operation(action) when is_atom(action) do + apply(__MODULE__, :"#{action}_operation", []) + end + + def get_state_root_operation, + do: ApiSpec.spec().paths["/eth/v1/beacon/states/{state_id}/root"].get + @spec get_state_root(Plug.Conn.t(), any) :: Plug.Conn.t() - def get_state_root(conn, %{"state_id" => state_id}) do + def get_state_root(conn, %{state_id: state_id}) do with {:ok, {root, execution_optimistic, finalized}} <- BeaconApi.Utils.parse_id(state_id) |> ForkChoice.Helpers.root_by_id(), {:ok, state_root} <- ForkChoice.Helpers.get_state_root(root) do @@ -25,28 +39,31 @@ defmodule BeaconApi.V1.BeaconController do end end + def get_block_root_operation, + do: ApiSpec.spec().paths["/eth/v1/beacon/blocks/{block_id}/root"].get + @spec get_block_root(Plug.Conn.t(), any) :: Plug.Conn.t() - def get_block_root(conn, %{"block_id" => "head"}) do + def get_block_root(conn, %{block_id: "head"}) do # TODO: determine head and return it conn |> block_not_found() end - def get_block_root(conn, %{"block_id" => "finalized"}) do + def get_block_root(conn, %{block_id: "finalized"}) do # TODO conn |> block_not_found() end - def get_block_root(conn, %{"block_id" => "justified"}) do + def get_block_root(conn, %{block_id: "justified"}) do # TODO conn |> block_not_found() end - def get_block_root(conn, %{"block_id" => "genesis"}) do + def get_block_root(conn, %{block_id: "genesis"}) do # TODO conn |> block_not_found() end - def get_block_root(conn, %{"block_id" => "0x" <> hex_block_id}) do + def get_block_root(conn, %{block_id: "0x" <> hex_block_id}) do with {:ok, block_root} <- Base.decode16(hex_block_id, case: :mixed), {:ok, _signed_block} <- BlockStore.get_block(block_root) do conn |> root_response(block_root, true, false) @@ -56,7 +73,7 @@ defmodule BeaconApi.V1.BeaconController do end end - def get_block_root(conn, %{"block_id" => block_id}) do + def get_block_root(conn, %{block_id: block_id}) do with {slot, ""} when slot >= 0 <- Integer.parse(block_id), {:ok, block_root} <- BlockStore.get_block_root_by_slot(slot) do conn |> root_response(block_root, true, false) diff --git a/lib/beacon_api/controllers/v2/beacon_controller.ex b/lib/beacon_api/controllers/v2/beacon_controller.ex index 365116da9..0bcf522a5 100644 --- a/lib/beacon_api/controllers/v2/beacon_controller.ex +++ b/lib/beacon_api/controllers/v2/beacon_controller.ex @@ -1,30 +1,43 @@ defmodule BeaconApi.V2.BeaconController do + alias BeaconApi.ApiSpec alias BeaconApi.ErrorController alias LambdaEthereumConsensus.Store.BlockStore use BeaconApi, :controller + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + @doc """ + action is an atom that correspond to the controller action's function atoms declared on `BeaconApi.Router` + """ + def open_api_operation(action) when is_atom(action) do + apply(__MODULE__, :"#{action}_operation", []) + end + + def get_block_operation, + do: ApiSpec.spec().paths["/eth/v2/beacon/blocks/{block_id}"].get + @spec get_block(Plug.Conn.t(), any) :: Plug.Conn.t() - def get_block(conn, %{"block_id" => "head"}) do + def get_block(conn, %{block_id: "head"}) do # TODO: determine head and return it conn |> block_not_found() end - def get_block(conn, %{"block_id" => "finalized"}) do + def get_block(conn, %{block_id: "finalized"}) do # TODO conn |> block_not_found() end - def get_block(conn, %{"block_id" => "justified"}) do + def get_block(conn, %{block_id: "justified"}) do # TODO conn |> block_not_found() end - def get_block(conn, %{"block_id" => "genesis"}) do + def get_block(conn, %{block_id: "genesis"}) do # TODO conn |> block_not_found() end - def get_block(conn, %{"block_id" => "0x" <> hex_block_id}) do + def get_block(conn, %{block_id: "0x" <> hex_block_id}) do with {:ok, block_root} <- Base.decode16(hex_block_id, case: :mixed), {:ok, block} <- BlockStore.get_block(block_root) do conn |> block_response(block) @@ -34,7 +47,7 @@ defmodule BeaconApi.V2.BeaconController do end end - def get_block(conn, %{"block_id" => block_id}) do + def get_block(conn, %{block_id: block_id}) do with {slot, ""} when slot >= 0 <- Integer.parse(block_id), {:ok, block} <- BlockStore.get_block_by_slot(slot) do conn |> block_response(block) diff --git a/lib/beacon_api/router.ex b/lib/beacon_api/router.ex index 57904ce2c..da06d17ad 100644 --- a/lib/beacon_api/router.ex +++ b/lib/beacon_api/router.ex @@ -3,6 +3,7 @@ defmodule BeaconApi.Router do pipeline :api do plug(:accepts, ["json"]) + plug(OpenApiSpex.Plug.PutApiSpec, module: BeaconApi.ApiSpec) end # Ethereum API Version 1 @@ -24,6 +25,11 @@ defmodule BeaconApi.Router do end end + scope "/api" do + pipe_through(:api) + get("/openapi", OpenApiSpex.Plug.RenderSpec, []) + end + # Catch-all route outside of any scope match(:*, "/*path", BeaconApi.ErrorController, :not_found) end diff --git a/mix.exs b/mix.exs index ceb809259..3cbe53c6f 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,8 @@ defmodule LambdaEthereumConsensus.MixProject do {:stream_data, "~> 0.5", only: [:test]}, {:benchee, "~> 1.2", only: [:dev]}, {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, - {:credo, "~> 1.7", only: [:dev, :test], runtime: false} + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:open_api_spex, "~> 3.18"} ] end diff --git a/mix.lock b/mix.lock index ab81c1a44..03bc33118 100644 --- a/mix.lock +++ b/mix.lock @@ -33,6 +33,7 @@ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "open_api_spex": {:hex, :open_api_spex, "3.18.1", "0a73cd5dbcba7d32952dd9738c6819892933d9bae1642f04c9f200281524dd31", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f52933cddecca675e42ead660379ae2d3853f57f5a35d201eaed85e2e81517d1"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "patch": {:hex, :patch, "0.12.0", "2da8967d382bade20344a3e89d618bfba563b12d4ac93955468e830777f816b0", [:mix], [], "hexpm", "ffd0e9a7f2ad5054f37af84067ee88b1ad337308a1cb227e181e3967127b0235"}, "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},