Skip to content

Commit b41ad31

Browse files
MarcosNicolauMauroToscanoJuArce
authored
feat: aggregation mode explorer (#1846)
Co-authored-by: MauroFab <[email protected]> Co-authored-by: Mauro Toscano <[email protected]> Co-authored-by: JuArce <[email protected]>
1 parent 37be9e4 commit b41ad31

File tree

14 files changed

+496
-7
lines changed

14 files changed

+496
-7
lines changed

explorer/.env.dev

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ DB_HOST=localhost
1414

1515
# Config file
1616
ALIGNED_CONFIG_FILE="../contracts/script/output/devnet/alignedlayer_deployment_output.json"
17+
ALIGNED_PROOF_AGG_CONFIG_FILE="../contracts/script/output/devnet/proof_aggregation_service_deployment_output.json"
18+
## Don't use public nodes as blob data can be retrieved from them
19+
BEACON_CLIENT="<beacon_client_url>"
1720

1821
# Debug
1922
DEBUG_ERRORS=true

explorer/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ DB_HOST=<db_host>
1414

1515
# Config file
1616
ALIGNED_CONFIG_FILE="<aligned_config_file>"
17+
ALIGNED_PROOF_AGG_CONFIG_FILE="<proof_aggregation_config_file>"
18+
## Don't use public nodes as blob data can be retrieved from them
19+
BEACON_CLIENT="<beacon_client_url>"
1720

1821
# Debug
1922
DEBUG_ERRORS=<true|false>

explorer/ecto_setup_db.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export DB_USER=$DB_USER
1010
export DB_PASS=$DB_PASS
1111
export DB_HOST=$DB_HOST
1212
export ALIGNED_CONFIG_FILE=$ALIGNED_CONFIG_FILE
13+
export ALIGNED_PROOF_AGG_CONFIG_FILE=$ALIGNED_PROOF_AGG_CONFIG_FILE
14+
export BEACON_CLIENT=$BEACON_CLIENT
1315

1416
mix deps.get
1517

explorer/lib/abi/AlignedProofAggregationService.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
defmodule Explorer.BeaconClient do
2+
require Logger
3+
@beacon_url System.get_env("BEACON_CLIENT")
4+
# See https://eips.ethereum.org/EIPS/eip-4844#parameters
5+
@versioned_hash_version_kzg 0x01
6+
7+
def fetch_blob_by_versioned_hash!(slot, blob_versioned_hash) do
8+
{:ok, blobs} = get_block_blobs(slot)
9+
data = Map.get(blobs, "data")
10+
11+
Enum.find(data, fn blob ->
12+
get_blob_versioned_hash(blob) == blob_versioned_hash
13+
end)
14+
end
15+
16+
def get_blob_versioned_hash(blob) do
17+
kzg_commitment = String.replace(Map.get(blob, "kzg_commitment"), "0x", "")
18+
kzg_commitment = Base.decode16!(kzg_commitment, case: :mixed)
19+
hash = Explorer.Utils.sha256_hash_raw(kzg_commitment)
20+
# See https://eips.ethereum.org/EIPS/eip-4844#helpers
21+
<<_first::8, rest::binary>> = hash
22+
raw = <<@versioned_hash_version_kzg::8>> <> rest
23+
"0x" <> Base.encode16(raw, case: :lower)
24+
end
25+
26+
def get_block_slot(beacon_block) do
27+
String.to_integer(
28+
beacon_block
29+
|> Map.get("data")
30+
|> Map.get("header")
31+
|> Map.get("message")
32+
|> Map.get("slot")
33+
)
34+
end
35+
36+
def get_block_header_by_hash(block_hash) do
37+
beacon_get("/eth/v1/beacon/headers/#{block_hash}")
38+
end
39+
40+
def get_block_header_by_parent_hash(parent_block_hash) do
41+
case beacon_get("/eth/v1/beacon/headers?parent_root=#{parent_block_hash}") do
42+
{:ok, header} ->
43+
data = header["data"] |> Enum.at(0)
44+
45+
{:ok, %{header | "data" => data}}
46+
47+
other ->
48+
other
49+
end
50+
end
51+
52+
def get_block_blobs(slot) do
53+
beacon_get("/eth/v1/beacon/blob_sidecars/#{slot}")
54+
end
55+
56+
defp beacon_get(method) do
57+
headers = [{"Content-Type", "application/json"}]
58+
request = Finch.build(:get, "#{@beacon_url}#{method}", headers)
59+
response = Finch.request(request, Explorer.Finch)
60+
61+
case response do
62+
{:ok, %Finch.Response{status: 200, body: body}} ->
63+
case Jason.decode(body) do
64+
{:ok, decoded_body} ->
65+
{:ok, decoded_body}
66+
67+
{:error, _} ->
68+
{:error, :invalid_json}
69+
end
70+
71+
{:ok, %Finch.Response{status: status}} ->
72+
{:error, status}
73+
74+
{:error, reason} ->
75+
{:error, reason}
76+
end
77+
end
78+
end
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule AlignedProofAggregationService do
2+
require Logger
3+
4+
@aligned_config_file System.get_env("ALIGNED_PROOF_AGG_CONFIG_FILE")
5+
6+
config_file_path =
7+
case @aligned_config_file do
8+
nil -> raise("ALIGNED_PROOF_AGG_CONFIG_FILE not set in .env")
9+
file -> file
10+
end
11+
12+
{status, config_json_string} = File.read(config_file_path)
13+
14+
case status do
15+
:ok ->
16+
Logger.debug("Aligned deployment file read successfully")
17+
18+
:error ->
19+
raise(
20+
"Config file not read successfully, make sure your .env is correctly created, and make sure Eigenlayer config file is correctly stored"
21+
)
22+
end
23+
24+
@contract_address Jason.decode!(config_json_string)
25+
|> Map.get("addresses")
26+
|> Map.get("alignedProofAggregationService")
27+
28+
use Ethers.Contract,
29+
abi_file: "lib/abi/AlignedProofAggregationService.json",
30+
default_address: @contract_address
31+
32+
def get_address() do
33+
@contract_address
34+
end
35+
36+
def get_aggregated_proof_event(%{from_block: fromBlock, to_block: toBlock}) do
37+
events =
38+
AlignedProofAggregationService.EventFilters.aggregated_proof_verified(nil)
39+
|> Ethers.get_logs(fromBlock: fromBlock, toBlock: toBlock)
40+
41+
case events do
42+
{:ok, []} ->
43+
{:ok, []}
44+
45+
{:ok, list} ->
46+
{:ok,
47+
Enum.map(list, fn x ->
48+
data = x |> Map.get(:data)
49+
topics_raw = x |> Map.get(:topics_raw)
50+
block_number = x |> Map.get(:block_number)
51+
tx_hash = x |> Map.get(:transaction_hash)
52+
53+
%{
54+
merkle_root:
55+
topics_raw
56+
|> Enum.at(1),
57+
blob_versioned_hash: "0x" <> Base.encode16(data |> Enum.at(0), case: :lower),
58+
block_number: block_number,
59+
block_timestamp: get_block_timestamp(block_number),
60+
tx_hash: tx_hash
61+
}
62+
end)}
63+
64+
{:error, reason} ->
65+
{:error, reason}
66+
end
67+
end
68+
69+
def get_block_timestamp(block_number) do
70+
case Ethers.Utils.get_block_timestamp(block_number) do
71+
{:ok, timestamp} -> DateTime.from_unix!(timestamp)
72+
{:error, error} -> raise("Error fetching block timestamp: #{error}")
73+
end
74+
end
75+
76+
def get_blob_data!(aggregated_proof) do
77+
{:ok, block} =
78+
Explorer.EthClient.get_block_by_number(
79+
Explorer.Utils.decimal_to_hex(aggregated_proof.block_number)
80+
)
81+
82+
parent_beacon_block_hash = Map.get(block, "parentBeaconBlockRoot")
83+
84+
{:ok, beacon_block} =
85+
Explorer.BeaconClient.get_block_header_by_parent_hash(parent_beacon_block_hash)
86+
87+
slot = Explorer.BeaconClient.get_block_slot(beacon_block)
88+
89+
data =
90+
Explorer.BeaconClient.fetch_blob_by_versioned_hash!(
91+
slot,
92+
aggregated_proof.blob_versioned_hash
93+
)
94+
95+
Map.get(data, "blob")
96+
end
97+
98+
@doc """
99+
Decodes blob data represented as an ASCII charlist.
100+
"""
101+
def decode_blob(blob_data), do: decode_blob(blob_data, [[]], 0, 0, 0)
102+
103+
defp decode_blob([], acc, _current_count, _total_count, _i), do: acc
104+
105+
defp decode_blob([head | tail], acc, current_count, total_count, i) do
106+
# Every 64 characters (or 32 bytes) there is a 00 for padding
107+
should_skip = rem(total_count, 64) == 0
108+
109+
case should_skip do
110+
true ->
111+
[_head | tail] = tail
112+
decode_blob(tail, acc, current_count, total_count + 2, i)
113+
114+
false ->
115+
acc = List.update_at(acc, i, fn chunk -> chunk ++ [head] end)
116+
117+
case current_count + 1 < 64 do
118+
true ->
119+
decode_blob(tail, acc, current_count + 1, total_count + 1, i)
120+
121+
false ->
122+
current_blob = Enum.at(acc, i)
123+
# 48 is 0 in ascii
124+
is_all_zeroes = Enum.all?(current_blob, fn x -> x == 48 end)
125+
126+
## If the hash is all zeroed, then there are no more hashes in the blob
127+
if is_all_zeroes do
128+
# Drop last limiter zeroed element
129+
Enum.drop(acc, -1)
130+
else
131+
decode_blob(tail, acc ++ [[]], 0, total_count + 1, i + 1)
132+
end
133+
end
134+
end
135+
end
136+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule Explorer.EthClient do
2+
require Logger
3+
@rpc_url System.get_env("RPC_URL")
4+
5+
def get_block_by_number(block_number) do
6+
eth_send("eth_getBlockByNumber", [block_number, false])
7+
end
8+
9+
defp eth_send(method, params, id \\ 1) do
10+
headers = [{"Content-Type", "application/json"}]
11+
body = Jason.encode!(%{jsonrpc: "2.0", method: method, params: params, id: id})
12+
request = Finch.build(:post, @rpc_url, headers, body)
13+
response = Finch.request(request, Explorer.Finch, [])
14+
15+
case response do
16+
{:ok, %Finch.Response{status: 200, body: body}} ->
17+
case Jason.decode(body) do
18+
{:ok, %{error: error} = _} -> {:error, error.message}
19+
{:ok, body} -> {:ok, Map.get(body, "result")}
20+
{:error, _} -> {:error, :invalid_json}
21+
end
22+
23+
{:ok, %Finch.Response{status: status}} ->
24+
{:error, status}
25+
26+
{:error, reason} ->
27+
{:error, reason}
28+
end
29+
end
30+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
defmodule AggregatedProofs do
2+
require Logger
3+
use Ecto.Schema
4+
import Ecto.Changeset
5+
6+
@primary_key {:id, :binary_id, autogenerate: true}
7+
schema "aggregated_proofs" do
8+
field(:merkle_root, :string)
9+
field(:blob_versioned_hash, :string)
10+
field(:block_number, :integer)
11+
field(:block_timestamp, :utc_datetime)
12+
field(:tx_hash, :string)
13+
field(:number_of_proofs, :integer)
14+
15+
has_many(:proofs_agg_mode, AggregationModeProof,
16+
foreign_key: :agg_proof_id,
17+
references: :id
18+
)
19+
20+
timestamps()
21+
end
22+
23+
@doc """
24+
Creates a changeset based on the given `attrs`.
25+
"""
26+
def changeset(aggregated_proof, attrs) do
27+
aggregated_proof
28+
|> cast(attrs, [
29+
:id,
30+
:merkle_root,
31+
:blob_versioned_hash,
32+
:block_number,
33+
:block_timestamp,
34+
:tx_hash,
35+
:number_of_proofs
36+
])
37+
|> validate_required([
38+
:merkle_root,
39+
:blob_versioned_hash,
40+
:block_number,
41+
:block_timestamp,
42+
:tx_hash,
43+
:number_of_proofs
44+
])
45+
|> unique_constraint(:id)
46+
end
47+
48+
def insert_or_update(agg_proof) do
49+
changeset = AggregatedProofs.changeset(%AggregatedProofs{}, agg_proof)
50+
51+
case Explorer.Repo.get_by(AggregatedProofs, block_number: agg_proof.block_number) do
52+
nil ->
53+
Explorer.Repo.insert(changeset)
54+
55+
existing_agg_proof ->
56+
"Updating aggregated proof" |> Logger.debug()
57+
58+
Ecto.Changeset.change(existing_agg_proof, changeset.changes)
59+
|> Explorer.Repo.update()
60+
end
61+
end
62+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule AggregationModeProof do
2+
require Logger
3+
use Ecto.Schema
4+
import Ecto.Changeset
5+
6+
# Different from proofs.ex (we could use the same but the hashes are constructed different)
7+
@primary_key {:id, :id, autogenerate: true}
8+
schema "proofs_agg_mode" do
9+
field(:agg_proof_id, :binary_id)
10+
field(:proof_hash, :string)
11+
field(:index, :integer)
12+
13+
belongs_to(:aggregated_proof, AggregatedProof,
14+
define_field: false,
15+
foreign_key: :agg_proof_id,
16+
references: :id,
17+
type: :binary_id
18+
)
19+
20+
timestamps()
21+
end
22+
23+
def changeset(proof, attrs) do
24+
proof
25+
|> cast(attrs, [:agg_proof_id, :proof_hash, :index])
26+
|> validate_required([:agg_proof_id, :proof_hash, :index])
27+
end
28+
29+
def insert_or_update(proof) do
30+
changeset =
31+
AggregationModeProof.changeset(%AggregationModeProof{}, proof)
32+
33+
case(
34+
Explorer.Repo.get_by(AggregationModeProof,
35+
agg_proof_id: proof.agg_proof_id,
36+
proof_hash: proof.proof_hash,
37+
index: proof.index
38+
)
39+
) do
40+
nil ->
41+
Explorer.Repo.insert(changeset)
42+
43+
existing_proof ->
44+
"Updating single aggregated proof" |> Logger.debug()
45+
46+
Ecto.Changeset.change(existing_proof, changeset.changes)
47+
|> Explorer.Repo.update()
48+
end
49+
end
50+
end

0 commit comments

Comments
 (0)